diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index e74e1bda..bbb99ce6 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -82,6 +82,7 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-debugging/SKILL.md` - `.github/skills/internal-devops-core-principles/SKILL.md` - `.github/skills/internal-docker/SKILL.md` +- `.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` @@ -168,10 +169,12 @@ These imported `openai-*` office skills remain support-only depth for repositori - `.github/scripts/graphify-file-change-hook.sh` - `.github/scripts/install-graphify-hooks.sh` - `.github/scripts/lib/catalog_checks.py` +- `.github/scripts/lib/cli_runner.py` - `.github/scripts/lib/critical_master.py` - `.github/scripts/lib/fingerprinting.py` - `.github/scripts/lib/internal_skills.py` - `.github/scripts/lib/inventory.py` +- `.github/scripts/lib/jsonc.py` - `.github/scripts/lib/shared.py` - `.github/scripts/lib/syncing.py` - `.github/scripts/lib/token_risks.py` @@ -183,6 +186,7 @@ These imported `openai-*` office skills remain support-only depth for repositori - `.github/scripts/validate_critical_output.py` - `.github/scripts/validate_critical_output.sh` - `.github/scripts/validate_internal_skills.py` +- `.github/scripts/validate_internal_skills.sh` ## Agents diff --git a/.github/README.md b/.github/README.md index 1dea25fe..352c2d71 100644 --- a/.github/README.md +++ b/.github/README.md @@ -3,9 +3,8 @@ This directory is the source-side catalog for reusable GitHub Copilot customization assets maintained in `cloud-strategy.github`. -- Root [`AGENTS.md`](../AGENTS.md) is the strategic entrypoint. -- `.github/copilot-instructions.md` is the compact repo-wide Copilot routing bridge. -- `.github/instructions/copilot-code-review.instructions.md` owns the global review baseline. +- Root [`AGENTS.md`](../AGENTS.md) is the strategic agent-policy entrypoint. +- `.github/copilot-instructions.md` is review-only for GitHub.com Copilot code review. - [`INVENTORY.md`](INVENTORY.md) is the exact path inventory for the live catalog. ## Agents 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 7fce6f47..ffd2f44e 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 @@ -21,7 +21,7 @@ Use this agent for route selection, mode selection, approval posture, and bounda ## Routing Rules - 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 bridge model around `AGENTS.md`, `.github/copilot-instructions.md`, `.github/instructions/copilot-code-review.instructions.md`, `.github/INVENTORY.md`, repository-root `LESSONS_LEARNED.md`, and explicitly shared hygiene files. +- 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 one-resource agent or skill authoring; recommend `internal-agent-creator` or `internal-skill-creator` as appropriate. @@ -38,7 +38,7 @@ Use this agent for route selection, mode selection, approval posture, and bounda ## Core Rules - Treat this repository as the source of truth for the managed sync baseline. -- Keep root guidance layered: `AGENTS.md` is the bridge, `.github/copilot-instructions.md` is the compact Copilot routing bridge, `.github/instructions/copilot-code-review.instructions.md` owns global review behavior, and `.github/INVENTORY.md` is the live catalog. +- Keep root guidance layered: `AGENTS.md` is the agent policy entrypoint, `.github/copilot-instructions.md` is review-only for GitHub.com Copilot code review, and `.github/INVENTORY.md` is the live catalog. - Keep target assumptions narrow and let the core skill own mirrored categories, exclusions, automation entrypoints, plan-file lifecycle, and validation sequence. - When repository-root `LESSONS_LEARNED.md` is in scope, preserve or migrate target-authored lesson rows through the core skill workflow. - When the source baseline includes approved imported-asset override registries or replay patches, mirror them as source-managed governance assets rather than creating target-local hidden forks. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a2043caf..c78c0894 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,31 +1,62 @@ -# Copilot Bridge - -Use `AGENTS.md` as the canonical repository bridge for policy, precedence, -ownership boundaries, and routing. - -Apply only matching instruction files from `.github/instructions/`. - -Select the smallest relevant skill from the prompt, target path, command -surface, validation signal, or repository evidence. - -Load task-specific skills or references only when workflow depth is needed. - -Do not edit imported upstream assets in place unless the need is strong, -explicit, and registered. - -Do not hardcode secrets. Use least privilege and evidence-first validation. - -When editing contracts, update validators/tests and run the closest available -checks. - -Report completed work with outcome, changed files, validation results, and -remaining gaps. - -Treat retained plans and `LESSONS_LEARNED.md` as non-canonical. - -Keep retained-plan workflow details in dedicated retained-plan skills and lesson-codification owners. - -Keep ledger row rules in their owned retained-learning files and skills. - -Include detailed resource sections only when the user asks or a narrower -contract requires them. +# GitHub.com Copilot Code Review + +This file is only for GitHub.com Copilot code review. + +It is not a general task-execution guide, repository routing guide, planning +workflow, or local agent runtime contract. + +## Review Objective + +Review changed files for defects that matter before merge. + +- Prioritize correctness, security, regressions, missing validation, and + maintainability. +- Prefer actionable findings over broad advice. +- Tie each finding to concrete changed-file evidence. +- Do not restate repository policy unless the diff creates a specific risk. + +## Finding Priority + +Use these buckets when reporting issues: + +- `Critical`: data loss, credential exposure, remote code execution, production + outage, or a merge-blocking contract break. +- `Major`: correctness bugs, security weaknesses, broken validators, missing + required tests, or behavior regressions. +- `Minor`: maintainability, edge-case, resilience, or observability issues that + should be fixed before merge when practical. +- `Nit`: small clarity or style issues that are safe to ignore. +- `Notes`: useful context that is not a defect. + +## Required Checks + +- Check for hardcoded secrets, credentials, keys, tokens, and tenant-sensitive + values. +- Check least privilege, destructive behavior controls, unsafe execution paths, + and missing input validation. +- Check whether changed behavior has appropriate tests, fixtures, docs, or + validators. +- Check contract alignment for changed schemas, generated assets, sync behavior, + prompts, instructions, skills, scripts, and CI workflows. +- Check that fixes are scoped to the requested behavior and do not rewrite large + unaffected areas. + +## Review Discipline + +- Report findings first, ordered by severity. +- For each finding, include the file or changed area, impact, and a concrete fix + direction. +- Avoid speculative findings when the diff does not provide enough evidence. +- Avoid praise, summaries, or style-only comments unless they reveal a real + maintenance risk. +- Escalate repeated instances of the same defect pattern when the repetition + increases risk. + +## Non-Scope + +- Do not provide implementation plans unless the review finding needs fix + guidance. +- Do not ask the author to follow local runtime workflows that GitHub.com cannot + execute. +- Do not treat this file as instructions for coding agents, local CLIs, or + non-review Copilot chat behavior. diff --git a/.github/instructions/internal-python.instructions.md b/.github/instructions/internal-python.instructions.md index 3ef22e5e..454d253d 100644 --- a/.github/instructions/internal-python.instructions.md +++ b/.github/instructions/internal-python.instructions.md @@ -15,8 +15,10 @@ This file is optimized for Copilot code review and should produce only evidenced - Do not flag stable domain invariants merely because they are constants near domain code. - Verify type hints and public interfaces stay consistent with call sites. - Flag manual formatting churn that fights the repository formatter; when Ruff is configured, prefer `ruff format` and Ruff diagnostics over subjective style edits. -- Report dependency usage that is unpinned or unnecessary for the change. +- Report dependency usage that is unpinned, unnecessary for the change, or unjustified for the data volume and import or install cost. - Flag vendored libraries, wheelhouses, copied site-packages, or fallback dependency mirrors. - Flag new external dependencies that are missing hash-locked pins in the owning requirements file. +- When behavior changes, verify docs and output contracts stay aligned: filenames, produced artifacts, columns, and other user-visible output details must match reality. - Check tests for deterministic coverage of changed behavior. -- Flag logging or exception messages that may leak sensitive values. +- Flag test setup that bypasses the repository's shared runner, pytest rootdir or testpaths contract, or declared interpreter or virtualenv setup without a reproducibility reason. +- Flag logging or exception messages that may leak sensitive values, raw request or response bodies, or secrets. diff --git a/.github/scripts/audit_copilot_catalog.py b/.github/scripts/audit_copilot_catalog.py index d0a80265..b74c477e 100644 --- a/.github/scripts/audit_copilot_catalog.py +++ b/.github/scripts/audit_copilot_catalog.py @@ -13,7 +13,8 @@ from pathlib import Path from lib.catalog_checks import run_consistency_checks -from lib.shared import Finding, find_repo_root, log_info, render_json +from lib.cli_runner import has_severity, run_finding_cli +from lib.shared import Finding, find_repo_root, log_info def parse_args() -> argparse.Namespace: @@ -26,14 +27,13 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() root = find_repo_root(Path(args.root)) - findings = run_consistency_checks(root, include_token_risks=True) - if args.format == "json": - print(render_json([finding.to_dict() for finding in findings])) - elif args.format == "compact": - print(render_json(build_compact_payload(findings))) - else: - render_text(findings) - return 1 if any(finding.severity == "blocking" for finding in findings) else 0 + findings = run_finding_cli( + detect_fn=lambda: run_consistency_checks(root, include_token_risks=True), + format_name=args.format, + render_text=render_text, + compact_builder=build_compact_payload, + ) + return 1 if has_severity(findings, "blocking") else 0 def render_text(findings: list[Finding]) -> None: diff --git a/.github/scripts/build_inventory.py b/.github/scripts/build_inventory.py index 4bcf4819..db15508b 100644 --- a/.github/scripts/build_inventory.py +++ b/.github/scripts/build_inventory.py @@ -12,7 +12,14 @@ from pathlib import Path from lib.inventory import build_inventory_markdown, write_inventory -from lib.shared import INVENTORY_PATH, find_repo_root, log_error, log_info, log_success, read_text +from lib.shared import ( + INVENTORY_PATH, + find_repo_root, + log_error, + log_info, + log_success, + read_text, +) def parse_args() -> argparse.Namespace: diff --git a/.github/scripts/check_catalog_consistency.py b/.github/scripts/check_catalog_consistency.py index fe8a6144..ef62a09d 100644 --- a/.github/scripts/check_catalog_consistency.py +++ b/.github/scripts/check_catalog_consistency.py @@ -13,7 +13,14 @@ from pathlib import Path from lib.catalog_checks import run_consistency_checks -from lib.shared import Finding, find_repo_root, log_error, log_success, log_warn, render_json +from lib.cli_runner import run_finding_cli, should_fail +from lib.shared import ( + Finding, + find_repo_root, + log_error, + log_success, + log_warn, +) def parse_args() -> argparse.Namespace: @@ -28,16 +35,16 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() root = find_repo_root(Path(args.root)) - findings = run_consistency_checks(root, include_token_risks=args.include_token_risks) - if args.format == "json": - print(render_json([finding.to_dict() for finding in findings])) - elif args.format == "compact": - print(render_json(build_compact_payload(findings))) - else: - render_text(findings) + findings = run_finding_cli( + detect_fn=lambda: run_consistency_checks( + root, include_token_risks=args.include_token_risks + ), + format_name=args.format, + render_text=render_text, + compact_builder=build_compact_payload, + ) - has_blocking = any(finding.severity == "blocking" for finding in findings) - if has_blocking or (args.strict and findings): + if should_fail(findings, strict=args.strict): return 1 if not findings: log_success("No consistency findings detected.") diff --git a/.github/scripts/detect_token_risks.py b/.github/scripts/detect_token_risks.py index 342cb194..eb1fd871 100644 --- a/.github/scripts/detect_token_risks.py +++ b/.github/scripts/detect_token_risks.py @@ -11,7 +11,8 @@ import argparse from pathlib import Path -from lib.shared import Finding, find_repo_root, log_warn, render_json +from lib.cli_runner import run_finding_cli, should_fail +from lib.shared import Finding, find_repo_root, log_warn from lib.token_risks import detect_token_risks @@ -26,12 +27,12 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() root = find_repo_root(Path(args.root)) - findings = detect_token_risks(root) - if args.format == "json": - print(render_json([finding.to_dict() for finding in findings])) - else: - render_text(findings) - return 1 if args.strict and findings else 0 + findings = run_finding_cli( + detect_fn=lambda: detect_token_risks(root), + format_name=args.format, + render_text=render_text, + ) + return 1 if should_fail(findings, strict=args.strict, blocking_severity=None) else 0 def render_text(findings: list[Finding]) -> None: diff --git a/.github/scripts/github_catalog_validation.py b/.github/scripts/github_catalog_validation.py index c8a3b193..8947e8ba 100644 --- a/.github/scripts/github_catalog_validation.py +++ b/.github/scripts/github_catalog_validation.py @@ -13,7 +13,14 @@ import subprocess from pathlib import Path -from lib.shared import find_repo_root, log_error, log_info, log_success, log_warn, render_json +from lib.shared import ( + find_repo_root, + log_error, + log_info, + log_success, + log_warn, + render_json, +) def parse_args() -> argparse.Namespace: diff --git a/.github/scripts/lib/catalog_checks.py b/.github/scripts/lib/catalog_checks.py index a72e4199..b392010f 100644 --- a/.github/scripts/lib/catalog_checks.py +++ b/.github/scripts/lib/catalog_checks.py @@ -1,21 +1,21 @@ from __future__ import annotations -from collections import defaultdict import re +from collections import defaultdict from pathlib import Path import yaml from .inventory import collect_inventory_sections, parse_inventory_markdown from .shared import ( - INVENTORY_PATH, + IGNORED_SYNC_FILENAMES, + IGNORED_SYNC_PARTS, IMPORTED_ASSET_OVERRIDES_PATH, + INVENTORY_PATH, LEGACY_AGENT_TOOL_IDS, SUPERPOWERS_NORMALIZATION_PATH, Finding, finding_sort_key, - IGNORED_SYNC_FILENAMES, - IGNORED_SYNC_PARTS, is_imported_asset, iter_markdown_assets, load_frontmatter, @@ -214,14 +214,28 @@ def check_bridge_references(root: Path) -> list[Finding]: ) if copilot_path.exists(): copilot_text = read_text(copilot_path) - if "AGENTS.md" not in copilot_text: + if "This file is only for GitHub.com Copilot code review." not in copilot_text: + findings.append( + Finding( + severity="blocking", + code="copilot-instructions-missing-review-scope", + path=".github/copilot-instructions.md", + message=".github/copilot-instructions.md must explicitly declare GitHub.com code-review-only scope.", + suggestion="Restore the review-only scope line in .github/copilot-instructions.md.", + ) + ) + + if ( + "Do not treat this file as instructions for coding agents, local CLIs, or" + not in copilot_text + ): findings.append( Finding( severity="blocking", - code="copilot-instructions-missing-agents-reference", + code="copilot-instructions-missing-runtime-boundary", path=".github/copilot-instructions.md", - message=".github/copilot-instructions.md no longer references AGENTS.md as the strategic bridge.", - suggestion="Restore the AGENTS.md reference to keep cross-surface precedence explicit.", + message=".github/copilot-instructions.md must keep the non-runtime boundary explicit.", + suggestion="Restore the non-runtime boundary line under the non-scope section.", ) ) return findings diff --git a/.github/scripts/lib/cli_runner.py b/.github/scripts/lib/cli_runner.py new file mode 100644 index 00000000..d3d94b47 --- /dev/null +++ b/.github/scripts/lib/cli_runner.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import Protocol, TypeVar + +from .shared import render_json + + +class FindingLike(Protocol): + severity: str + + def to_dict(self) -> dict[str, object]: ... + + +FindingT = TypeVar("FindingT", bound=FindingLike) + + +def run_finding_cli( + *, + detect_fn: Callable[[], list[FindingT]], + format_name: str, + render_text: Callable[[list[FindingT]], None], + compact_builder: Callable[[list[FindingT]], dict[str, object]] | None = None, +) -> list[FindingT]: + findings = detect_fn() + if format_name == "json": + print(render_json([finding.to_dict() for finding in findings])) + elif format_name == "compact": + if compact_builder is None: + raise ValueError("compact output requires a compact_builder") + print(render_json(compact_builder(findings))) + else: + render_text(findings) + return findings + + +def has_severity(findings: list[FindingLike], severity: str) -> bool: + return any(finding.severity == severity for finding in findings) + + +def should_fail( + findings: list[FindingLike], + *, + strict: bool = False, + blocking_severity: str | None = "blocking", +) -> bool: + if blocking_severity is not None and has_severity(findings, blocking_severity): + return True + return strict and bool(findings) diff --git a/.github/scripts/lib/jsonc.py b/.github/scripts/lib/jsonc.py new file mode 100644 index 00000000..cbf521e5 --- /dev/null +++ b/.github/scripts/lib/jsonc.py @@ -0,0 +1,333 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass + +from .shared import VSCODE_COPILOT_SETTINGS + + +@dataclass(frozen=True) +class JsoncObjectMember: + key: str + value_kind: str + value_start: int + value_end: int + object_value: JsoncObjectInfo | None + has_trailing_comma: bool + + +@dataclass(frozen=True) +class JsoncObjectInfo: + start: int + end: int + members: tuple[JsoncObjectMember, ...] + + +class JsoncParseError(ValueError): + pass + + +def skip_jsonc_space_and_comments(text: str, index: int) -> int: + length = len(text) + cursor = index + while cursor < length: + char = text[cursor] + if char in {" ", "\t", "\n", "\r"}: + cursor += 1 + continue + if text.startswith("//", cursor): + cursor += 2 + while cursor < length and text[cursor] not in {"\n", "\r"}: + cursor += 1 + continue + if text.startswith("/*", cursor): + end = text.find("*/", cursor + 2) + if end == -1: + raise JsoncParseError("unterminated block comment") + cursor = end + 2 + continue + break + return cursor + + +def consume_jsonc_string(text: str, index: int) -> tuple[str, int]: + if index >= len(text) or text[index] != '"': + raise JsoncParseError("expected string") + cursor = index + 1 + chars: list[str] = [] + while cursor < len(text): + char = text[cursor] + if char == '"': + return "".join(chars), cursor + 1 + if char == "\\": + cursor += 1 + if cursor >= len(text): + raise JsoncParseError("unterminated string escape") + escaped = text[cursor] + if escaped == "u": + hex_chunk = text[cursor + 1 : cursor + 5] + if len(hex_chunk) != 4 or not all( + glyph in "0123456789abcdefABCDEF" for glyph in hex_chunk + ): + raise JsoncParseError("invalid unicode escape") + chars.append(chr(int(hex_chunk, 16))) + cursor += 5 + continue + escaped_map = { + '"': '"', + "\\": "\\", + "/": "/", + "b": "\b", + "f": "\f", + "n": "\n", + "r": "\r", + "t": "\t", + } + if escaped not in escaped_map: + raise JsoncParseError("invalid escape sequence") + chars.append(escaped_map[escaped]) + cursor += 1 + continue + chars.append(char) + cursor += 1 + raise JsoncParseError("unterminated string") + + +def consume_jsonc_scalar(text: str, index: int) -> int: + cursor = index + while cursor < len(text): + char = text[cursor] + if char in {",", "}", "]"}: + break + if char in {" ", "\t", "\n", "\r"}: + break + if text.startswith("//", cursor) or text.startswith("/*", cursor): + break + cursor += 1 + if cursor == index: + raise JsoncParseError("expected scalar value") + return cursor + + +def parse_jsonc_value( + text: str, + index: int, +) -> tuple[str, int, int, JsoncObjectInfo | None, int]: + cursor = skip_jsonc_space_and_comments(text, index) + if cursor >= len(text): + raise JsoncParseError("expected value") + + char = text[cursor] + if char == "{": + object_info, next_cursor = parse_jsonc_object(text, cursor) + return "object", cursor, next_cursor, object_info, next_cursor + if char == "[": + next_cursor = parse_jsonc_array(text, cursor) + return "array", cursor, next_cursor, None, next_cursor + if char == '"': + _, next_cursor = consume_jsonc_string(text, cursor) + return "string", cursor, next_cursor, None, next_cursor + + next_cursor = consume_jsonc_scalar(text, cursor) + token = text[cursor:next_cursor] + if token in {"true", "false", "null"}: + return "literal", cursor, next_cursor, None, next_cursor + + if re.fullmatch(r"-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?", token): + return "number", cursor, next_cursor, None, next_cursor + raise JsoncParseError(f"invalid token '{token}'") + + +def parse_jsonc_array(text: str, index: int) -> int: + cursor = index + 1 + expect_value = True + while True: + cursor = skip_jsonc_space_and_comments(text, cursor) + if cursor >= len(text): + raise JsoncParseError("unterminated array") + if text[cursor] == ']': + return cursor + 1 + if not expect_value: + raise JsoncParseError("expected ',' or ']' in array") + + _, _, _, _, cursor = parse_jsonc_value(text, cursor) + cursor = skip_jsonc_space_and_comments(text, cursor) + if cursor >= len(text): + raise JsoncParseError("unterminated array") + if text[cursor] == ',': + cursor += 1 + expect_value = True + continue + if text[cursor] == ']': + return cursor + 1 + raise JsoncParseError("expected ',' or ']' in array") + + +def parse_jsonc_object(text: str, index: int) -> tuple[JsoncObjectInfo, int]: + cursor = index + 1 + members: list[JsoncObjectMember] = [] + + while True: + cursor = skip_jsonc_space_and_comments(text, cursor) + if cursor >= len(text): + raise JsoncParseError("unterminated object") + if text[cursor] == '}': + return JsoncObjectInfo(start=index, end=cursor, members=tuple(members)), cursor + 1 + + key, after_key = consume_jsonc_string(text, cursor) + cursor = skip_jsonc_space_and_comments(text, after_key) + if cursor >= len(text) or text[cursor] != ':': + raise JsoncParseError("expected ':' after object key") + cursor += 1 + + value_kind, value_start, value_end, value_object, cursor = parse_jsonc_value( + text, cursor + ) + cursor = skip_jsonc_space_and_comments(text, cursor) + has_trailing_comma = False + if cursor < len(text) and text[cursor] == ',': + has_trailing_comma = True + cursor += 1 + + members.append( + JsoncObjectMember( + key=key, + value_kind=value_kind, + value_start=value_start, + value_end=value_end, + object_value=value_object, + has_trailing_comma=has_trailing_comma, + ) + ) + + if has_trailing_comma: + continue + + cursor = skip_jsonc_space_and_comments(text, cursor) + if cursor >= len(text): + raise JsoncParseError("unterminated object") + if text[cursor] == '}': + return JsoncObjectInfo(start=index, end=cursor, members=tuple(members)), cursor + 1 + raise JsoncParseError("expected ',' or '}' in object") + + +def parse_jsonc_root_object(text: str) -> JsoncObjectInfo: + cursor = skip_jsonc_space_and_comments(text, 0) + if cursor >= len(text) or text[cursor] != '{': + raise JsoncParseError("settings content must start with a JSON object") + root, next_cursor = parse_jsonc_object(text, cursor) + trailing = skip_jsonc_space_and_comments(text, next_cursor) + if trailing != len(text): + raise JsoncParseError("unexpected trailing content after root object") + return root + + +def object_members_by_key(object_info: JsoncObjectInfo, key: str) -> list[JsoncObjectMember]: + return [member for member in object_info.members if member.key == key] + + +def object_indent(text: str, object_info: JsoncObjectInfo) -> str: + line_start = text.rfind("\n", 0, object_info.start) + 1 + prefix = text[line_start:object_info.start] + return re.match(r"[ \t]*", prefix).group(0) if prefix else "" + + +def member_indent(text: str, object_info: JsoncObjectInfo) -> str: + for member in object_info.members: + line_start = text.rfind("\n", 0, member.value_start) + 1 + indent = re.match(r"[ \t]*", text[line_start:member.value_start]).group(0) + if indent: + return indent + return f"{object_indent(text, object_info)} " + + +def insert_object_member(text: str, object_info: JsoncObjectInfo, member_text: str) -> str: + indent = member_indent(text, object_info) + if not object_info.members: + base_indent = object_indent(text, object_info) + insertion = f"\n{indent}{member_text}\n{base_indent}" + return text[: object_info.start + 1] + insertion + text[object_info.end :] + + last_member = object_info.members[-1] + separator = "" if last_member.has_trailing_comma else "," + insertion = f"{separator}\n{indent}{member_text}" + return text[: object_info.end] + insertion + text[object_info.end :] + + +def ensure_root_boolean_setting(text: str, root: JsoncObjectInfo, key: str) -> str: + matches = object_members_by_key(root, key) + if len(matches) > 1: + raise JsoncParseError(f"duplicate key '{key}' is not safe to auto-merge") + if not matches: + return insert_object_member(text, root, f'"{key}": false') + + member = matches[0] + value = text[member.value_start : member.value_end].strip() + if value == "false": + return text + if value != "true": + raise JsoncParseError( + f"managed key '{key}' must be a boolean for safe merge" + ) + return text[: member.value_start] + "false" + text[member.value_end :] + + +def ensure_nested_boolean_setting( + text: str, + root: JsoncObjectInfo, + object_key: str, + nested_key: str, +) -> str: + matches = object_members_by_key(root, object_key) + if len(matches) > 1: + raise JsoncParseError( + f"duplicate key '{object_key}' is not safe to auto-merge" + ) + + if not matches: + nested_object = f'"{object_key}": {{\n "{nested_key}": false\n }}' + return insert_object_member(text, root, nested_object) + + member = matches[0] + if member.value_kind != "object" or member.object_value is None: + raise JsoncParseError( + f"managed key '{object_key}' must be an object for safe merge" + ) + + nested_matches = object_members_by_key(member.object_value, nested_key) + if len(nested_matches) > 1: + raise JsoncParseError( + f"duplicate key '{nested_key}' inside '{object_key}' is not safe to auto-merge" + ) + + if not nested_matches: + return insert_object_member(text, member.object_value, f'"{nested_key}": false') + + nested_member = nested_matches[0] + nested_value = text[nested_member.value_start : nested_member.value_end].strip() + if nested_value == "false": + return text + if nested_value != "true": + raise JsoncParseError( + f"managed key '{object_key}.{nested_key}' must be a boolean for safe merge" + ) + return text[: nested_member.value_start] + "false" + text[nested_member.value_end :] + + +def apply_managed_vscode_copilot_settings(content: str) -> str: + updated = content + root = parse_jsonc_root_object(updated) + updated = ensure_root_boolean_setting( + updated, + root, + VSCODE_COPILOT_SETTINGS[0][0][0], + ) + + root = parse_jsonc_root_object(updated) + updated = ensure_nested_boolean_setting( + updated, + root, + VSCODE_COPILOT_SETTINGS[1][0][0], + VSCODE_COPILOT_SETTINGS[1][0][1], + ) + return updated diff --git a/.github/scripts/lib/syncing.py b/.github/scripts/lib/syncing.py index b972cfc2..e8e72c9c 100644 --- a/.github/scripts/lib/syncing.py +++ b/.github/scripts/lib/syncing.py @@ -3,13 +3,14 @@ import hashlib import json import re -from datetime import datetime, timezone from dataclasses import dataclass +from datetime import datetime, timezone from pathlib import Path from shutil import copy2 -from .inventory import render_inventory_markdown, sections_from_catalog_paths from .fingerprinting import HASH_ALGO, NORMALIZATION_VERSION, build_fingerprint +from .inventory import render_inventory_markdown, sections_from_catalog_paths +from .jsonc import JsoncParseError, apply_managed_vscode_copilot_settings from .shared import ( ARCHITECTURE_PATH, CONSUMER_LOCAL_KNOWLEDGE_TEMPLATES, @@ -79,24 +80,42 @@ class PendingLessonsTable: @dataclass(frozen=True) -class JsoncObjectMember: - key: str - value_kind: str - value_start: int - value_end: int - object_value: JsoncObjectInfo | None - has_trailing_comma: bool - - -@dataclass(frozen=True) -class JsoncObjectInfo: - start: int - end: int - members: tuple[JsoncObjectMember, ...] - - -class JsoncParseError(ValueError): - pass +class ConsumerLocalKnowledgeSpec: + path: str + preserve_reason: str + create_reason: str + legacy_paths: tuple[str, ...] = () + + +CONSUMER_LOCAL_KNOWLEDGE_SPECS = ( + ConsumerLocalKnowledgeSpec( + path=DOCS_README_PATH, + preserve_reason="Preserved consumer-local docs guide after scaffold materialization.", + create_reason="Consumer-local docs guide missing; create scaffold from the source template.", + ), + ConsumerLocalKnowledgeSpec( + path=ARCHITECTURE_PATH, + preserve_reason="Preserved consumer-local architecture contract after scaffold materialization.", + create_reason="Consumer-local architecture contract missing; create scaffold from the source template.", + legacy_paths=ARCHITECTURE_LEGACY_PATHS, + ), + ConsumerLocalKnowledgeSpec( + path=REPOSITORY_CONTEXT_PATH, + preserve_reason="Preserved consumer-local repository context after scaffold materialization.", + create_reason="Consumer-local repository context missing; create scaffold from the source template.", + legacy_paths=REPOSITORY_CONTEXT_LEGACY_PATHS, + ), + ConsumerLocalKnowledgeSpec( + path=TECH_PATH, + preserve_reason="Preserved consumer-local technology document after scaffold materialization.", + create_reason="Consumer-local technology document missing; create scaffold from the source template.", + ), + ConsumerLocalKnowledgeSpec( + path=STRUCTURE_PATH, + preserve_reason="Preserved consumer-local structure document after scaffold materialization.", + create_reason="Consumer-local structure document missing; create scaffold from the source template.", + ), +) def append_vscode_settings_operation( @@ -168,316 +187,6 @@ def render_minimal_vscode_settings_jsonc() -> str: ) -def skip_jsonc_space_and_comments(text: str, index: int) -> int: - length = len(text) - cursor = index - while cursor < length: - char = text[cursor] - if char in {" ", "\t", "\n", "\r"}: - cursor += 1 - continue - if text.startswith("//", cursor): - cursor += 2 - while cursor < length and text[cursor] not in {"\n", "\r"}: - cursor += 1 - continue - if text.startswith("/*", cursor): - end = text.find("*/", cursor + 2) - if end == -1: - raise JsoncParseError("unterminated block comment") - cursor = end + 2 - continue - break - return cursor - - -def consume_jsonc_string(text: str, index: int) -> tuple[str, int]: - if index >= len(text) or text[index] != '"': - raise JsoncParseError("expected string") - cursor = index + 1 - chars: list[str] = [] - while cursor < len(text): - char = text[cursor] - if char == '"': - return "".join(chars), cursor + 1 - if char == "\\": - cursor += 1 - if cursor >= len(text): - raise JsoncParseError("unterminated string escape") - escaped = text[cursor] - if escaped == "u": - hex_chunk = text[cursor + 1 : cursor + 5] - if len(hex_chunk) != 4 or not all( - glyph in "0123456789abcdefABCDEF" for glyph in hex_chunk - ): - raise JsoncParseError("invalid unicode escape") - chars.append(chr(int(hex_chunk, 16))) - cursor += 5 - continue - escaped_map = { - '"': '"', - "\\": "\\", - "/": "/", - "b": "\b", - "f": "\f", - "n": "\n", - "r": "\r", - "t": "\t", - } - if escaped not in escaped_map: - raise JsoncParseError("invalid escape sequence") - chars.append(escaped_map[escaped]) - cursor += 1 - continue - chars.append(char) - cursor += 1 - raise JsoncParseError("unterminated string") - - -def consume_jsonc_scalar(text: str, index: int) -> int: - cursor = index - while cursor < len(text): - char = text[cursor] - if char in {",", "}", "]"}: - break - if char in {" ", "\t", "\n", "\r"}: - break - if text.startswith("//", cursor) or text.startswith("/*", cursor): - break - cursor += 1 - if cursor == index: - raise JsoncParseError("expected scalar value") - return cursor - - -def parse_jsonc_value( - text: str, - index: int, -) -> tuple[str, int, int, JsoncObjectInfo | None, int]: - cursor = skip_jsonc_space_and_comments(text, index) - if cursor >= len(text): - raise JsoncParseError("expected value") - - char = text[cursor] - if char == '{': - object_info, next_cursor = parse_jsonc_object(text, cursor) - return "object", cursor, next_cursor, object_info, next_cursor - if char == '[': - next_cursor = parse_jsonc_array(text, cursor) - return "array", cursor, next_cursor, None, next_cursor - if char == '"': - _, next_cursor = consume_jsonc_string(text, cursor) - return "string", cursor, next_cursor, None, next_cursor - - next_cursor = consume_jsonc_scalar(text, cursor) - token = text[cursor:next_cursor] - if token in {"true", "false", "null"}: - return "literal", cursor, next_cursor, None, next_cursor - - if re.fullmatch(r"-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?", token): - return "number", cursor, next_cursor, None, next_cursor - raise JsoncParseError(f"invalid token '{token}'") - - -def parse_jsonc_array(text: str, index: int) -> int: - cursor = index + 1 - expect_value = True - while True: - cursor = skip_jsonc_space_and_comments(text, cursor) - if cursor >= len(text): - raise JsoncParseError("unterminated array") - if text[cursor] == ']': - return cursor + 1 - if not expect_value: - raise JsoncParseError("expected ',' or ']' in array") - - _, _, _, _, cursor = parse_jsonc_value(text, cursor) - cursor = skip_jsonc_space_and_comments(text, cursor) - if cursor >= len(text): - raise JsoncParseError("unterminated array") - if text[cursor] == ',': - cursor += 1 - expect_value = True - continue - if text[cursor] == ']': - return cursor + 1 - raise JsoncParseError("expected ',' or ']' in array") - - -def parse_jsonc_object(text: str, index: int) -> tuple[JsoncObjectInfo, int]: - cursor = index + 1 - members: list[JsoncObjectMember] = [] - - while True: - cursor = skip_jsonc_space_and_comments(text, cursor) - if cursor >= len(text): - raise JsoncParseError("unterminated object") - if text[cursor] == '}': - return JsoncObjectInfo(start=index, end=cursor, members=tuple(members)), cursor + 1 - - key, after_key = consume_jsonc_string(text, cursor) - cursor = skip_jsonc_space_and_comments(text, after_key) - if cursor >= len(text) or text[cursor] != ':': - raise JsoncParseError("expected ':' after object key") - cursor += 1 - - value_kind, value_start, value_end, value_object, cursor = parse_jsonc_value( - text, cursor - ) - cursor = skip_jsonc_space_and_comments(text, cursor) - has_trailing_comma = False - if cursor < len(text) and text[cursor] == ',': - has_trailing_comma = True - cursor += 1 - - members.append( - JsoncObjectMember( - key=key, - value_kind=value_kind, - value_start=value_start, - value_end=value_end, - object_value=value_object, - has_trailing_comma=has_trailing_comma, - ) - ) - - if has_trailing_comma: - continue - - cursor = skip_jsonc_space_and_comments(text, cursor) - if cursor >= len(text): - raise JsoncParseError("unterminated object") - if text[cursor] == '}': - return JsoncObjectInfo(start=index, end=cursor, members=tuple(members)), cursor + 1 - raise JsoncParseError("expected ',' or '}' in object") - - -def parse_jsonc_root_object(text: str) -> JsoncObjectInfo: - cursor = skip_jsonc_space_and_comments(text, 0) - if cursor >= len(text) or text[cursor] != '{': - raise JsoncParseError("settings content must start with a JSON object") - root, next_cursor = parse_jsonc_object(text, cursor) - trailing = skip_jsonc_space_and_comments(text, next_cursor) - if trailing != len(text): - raise JsoncParseError("unexpected trailing content after root object") - return root - - -def object_members_by_key(object_info: JsoncObjectInfo, key: str) -> list[JsoncObjectMember]: - return [member for member in object_info.members if member.key == key] - - -def object_indent(text: str, object_info: JsoncObjectInfo) -> str: - line_start = text.rfind("\n", 0, object_info.start) + 1 - prefix = text[line_start:object_info.start] - return re.match(r"[ \t]*", prefix).group(0) if prefix else "" - - -def member_indent(text: str, object_info: JsoncObjectInfo) -> str: - for member in object_info.members: - line_start = text.rfind("\n", 0, member.value_start) + 1 - indent = re.match(r"[ \t]*", text[line_start:member.value_start]).group(0) - if indent: - return indent - return f"{object_indent(text, object_info)} " - - -def insert_object_member(text: str, object_info: JsoncObjectInfo, member_text: str) -> str: - indent = member_indent(text, object_info) - if not object_info.members: - base_indent = object_indent(text, object_info) - insertion = f"\n{indent}{member_text}\n{base_indent}" - return text[: object_info.start + 1] + insertion + text[object_info.end :] - - last_member = object_info.members[-1] - separator = "" if last_member.has_trailing_comma else "," - insertion = f"{separator}\n{indent}{member_text}" - return text[: object_info.end] + insertion + text[object_info.end :] - - -def ensure_root_boolean_setting( - text: str, - root: JsoncObjectInfo, - key: str, -) -> str: - matches = object_members_by_key(root, key) - if len(matches) > 1: - raise JsoncParseError(f"duplicate key '{key}' is not safe to auto-merge") - if not matches: - return insert_object_member(text, root, f'"{key}": false') - - member = matches[0] - value = text[member.value_start : member.value_end].strip() - if value == "false": - return text - if value != "true": - raise JsoncParseError( - f"managed key '{key}' must be a boolean for safe merge" - ) - return text[: member.value_start] + "false" + text[member.value_end :] - - -def ensure_nested_boolean_setting( - text: str, - root: JsoncObjectInfo, - object_key: str, - nested_key: str, -) -> str: - matches = object_members_by_key(root, object_key) - if len(matches) > 1: - raise JsoncParseError( - f"duplicate key '{object_key}' is not safe to auto-merge" - ) - - if not matches: - nested_object = f'"{object_key}": {{\n "{nested_key}": false\n }}' - return insert_object_member(text, root, nested_object) - - member = matches[0] - if member.value_kind != "object" or member.object_value is None: - raise JsoncParseError( - f"managed key '{object_key}' must be an object for safe merge" - ) - - nested_matches = object_members_by_key(member.object_value, nested_key) - if len(nested_matches) > 1: - raise JsoncParseError( - f"duplicate key '{nested_key}' inside '{object_key}' is not safe to auto-merge" - ) - - if not nested_matches: - return insert_object_member(text, member.object_value, f'"{nested_key}": false') - - nested_member = nested_matches[0] - nested_value = text[nested_member.value_start : nested_member.value_end].strip() - if nested_value == "false": - return text - if nested_value != "true": - raise JsoncParseError( - f"managed key '{object_key}.{nested_key}' must be a boolean for safe merge" - ) - return text[: nested_member.value_start] + "false" + text[nested_member.value_end :] - - -def apply_managed_vscode_copilot_settings(content: str) -> str: - updated = content - root = parse_jsonc_root_object(updated) - updated = ensure_root_boolean_setting( - updated, - root, - VSCODE_COPILOT_SETTINGS[0][0][0], - ) - - root = parse_jsonc_root_object(updated) - updated = ensure_nested_boolean_setting( - updated, - root, - VSCODE_COPILOT_SETTINGS[1][0][0], - VSCODE_COPILOT_SETTINGS[1][0][1], - ) - return updated - - def build_sync_plan(source_root: Path, target_root: Path) -> SyncPlan: source_root = source_root.resolve() target_root = target_root.resolve() @@ -769,181 +478,55 @@ def append_consumer_local_knowledge_operations( operations: list[SyncOperation], local_assets: list[str], ) -> None: - append_docs_readme_operations(source_root, target_root, operations, local_assets) - append_architecture_operations(source_root, target_root, operations, local_assets) - append_repository_context_operations( - source_root, target_root, operations, local_assets - ) - append_tech_operations(source_root, target_root, operations, local_assets) - append_structure_operations(source_root, target_root, operations, local_assets) - - -def append_docs_readme_operations( - source_root: Path, - target_root: Path, - operations: list[SyncOperation], - local_assets: list[str], -) -> None: - template_path = source_root / CONSUMER_LOCAL_KNOWLEDGE_TEMPLATES[DOCS_README_PATH] - target_path = target_root / DOCS_README_PATH - - if target_path.exists(): - local_assets.append(DOCS_README_PATH) - operations.append( - SyncOperation( - action="preserve", - path=DOCS_README_PATH, - reason="Preserved consumer-local docs guide after scaffold materialization.", - source_hash=( - sha256_file(template_path) if template_path.exists() else None - ), - target_hash=sha256_file(target_path), - ) - ) - return - - if template_path.exists(): - operations.append( - SyncOperation( - action="create", - path=DOCS_README_PATH, - reason="Consumer-local docs guide missing; create scaffold from the source template.", - source_hash=sha256_file(template_path), - target_hash=None, - ) - ) - - -def append_architecture_operations( - source_root: Path, - target_root: Path, - operations: list[SyncOperation], - local_assets: list[str], -) -> None: - template_path = source_root / CONSUMER_LOCAL_KNOWLEDGE_TEMPLATES[ARCHITECTURE_PATH] - target_path = target_root / ARCHITECTURE_PATH - legacy_paths = [ - legacy_path - for legacy_path in ARCHITECTURE_LEGACY_PATHS - if (target_root / legacy_path).exists() - ] - - if target_path.exists() and legacy_paths: - local_assets.append(ARCHITECTURE_PATH) - operations.append( - SyncOperation( - action="manual", - path=ARCHITECTURE_PATH, - reason=( - f"Both canonical {ARCHITECTURE_PATH} and legacy {', '.join(legacy_paths)} exist; reconcile manually before apply." - ), - source_hash=( - sha256_file(template_path) if template_path.exists() else None - ), - target_hash=sha256_file(target_path), - ) - ) - return - - if target_path.exists(): - local_assets.append(ARCHITECTURE_PATH) - operations.append( - SyncOperation( - action="preserve", - path=ARCHITECTURE_PATH, - reason="Preserved consumer-local architecture contract after scaffold materialization.", - source_hash=( - sha256_file(template_path) if template_path.exists() else None - ), - target_hash=sha256_file(target_path), - ) + for spec in CONSUMER_LOCAL_KNOWLEDGE_SPECS: + _append_consumer_knowledge_operation( + source_root=source_root, + target_root=target_root, + operations=operations, + local_assets=local_assets, + spec=spec, ) - return - if len(legacy_paths) > 1: - operations.append( - SyncOperation( - action="manual", - path=ARCHITECTURE_PATH, - reason=( - f"Multiple legacy paths for {ARCHITECTURE_PATH} exist ({', '.join(legacy_paths)}); reconcile manually before apply." - ), - source_hash=( - sha256_file(template_path) if template_path.exists() else None - ), - target_hash=sha256_file(target_root / legacy_paths[0]), - ) - ) - return - if legacy_paths: - legacy_path = legacy_paths[0] - operations.append( - SyncOperation( - action="rename", - path=ARCHITECTURE_PATH, - reason=f"Legacy {legacy_path} should move to consumer-local {ARCHITECTURE_PATH}.", - source_hash=None, - target_hash=sha256_file(target_root / legacy_path), - ) - ) - return - - if template_path.exists(): - operations.append( - SyncOperation( - action="create", - path=ARCHITECTURE_PATH, - reason="Consumer-local architecture contract missing; create scaffold from the source template.", - source_hash=sha256_file(template_path), - target_hash=None, - ) - ) - - -def append_repository_context_operations( +def _append_consumer_knowledge_operation( source_root: Path, target_root: Path, operations: list[SyncOperation], local_assets: list[str], + spec: ConsumerLocalKnowledgeSpec, ) -> None: - template_path = ( - source_root / CONSUMER_LOCAL_KNOWLEDGE_TEMPLATES[REPOSITORY_CONTEXT_PATH] - ) - target_path = target_root / REPOSITORY_CONTEXT_PATH + template_path = source_root / CONSUMER_LOCAL_KNOWLEDGE_TEMPLATES[spec.path] + target_path = target_root / spec.path + source_hash = sha256_file(template_path) if template_path.exists() else None legacy_paths = [ legacy_path - for legacy_path in REPOSITORY_CONTEXT_LEGACY_PATHS + for legacy_path in spec.legacy_paths if (target_root / legacy_path).exists() ] if target_path.exists() and legacy_paths: - local_assets.append(REPOSITORY_CONTEXT_PATH) + local_assets.append(spec.path) operations.append( SyncOperation( action="manual", - path=REPOSITORY_CONTEXT_PATH, + path=spec.path, reason=( - f"Both canonical {REPOSITORY_CONTEXT_PATH} and legacy {', '.join(legacy_paths)} exist; reconcile manually before apply." - ), - source_hash=( - sha256_file(template_path) if template_path.exists() else None + f"Both canonical {spec.path} and legacy {', '.join(legacy_paths)} exist; reconcile manually before apply." ), + source_hash=source_hash, target_hash=sha256_file(target_path), ) ) return if target_path.exists(): - local_assets.append(REPOSITORY_CONTEXT_PATH) + local_assets.append(spec.path) operations.append( SyncOperation( action="preserve", - path=REPOSITORY_CONTEXT_PATH, - reason="Preserved consumer-local repository context after scaffold materialization.", - source_hash=( - sha256_file(template_path) if template_path.exists() else None - ), + path=spec.path, + reason=spec.preserve_reason, + source_hash=source_hash, target_hash=sha256_file(target_path), ) ) @@ -953,13 +536,11 @@ def append_repository_context_operations( operations.append( SyncOperation( action="manual", - path=REPOSITORY_CONTEXT_PATH, + path=spec.path, reason=( - f"Multiple legacy paths for {REPOSITORY_CONTEXT_PATH} exist ({', '.join(legacy_paths)}); reconcile manually before apply." - ), - source_hash=( - sha256_file(template_path) if template_path.exists() else None + f"Multiple legacy paths for {spec.path} exist ({', '.join(legacy_paths)}); reconcile manually before apply." ), + source_hash=source_hash, target_hash=sha256_file(target_root / legacy_paths[0]), ) ) @@ -970,8 +551,8 @@ def append_repository_context_operations( operations.append( SyncOperation( action="rename", - path=REPOSITORY_CONTEXT_PATH, - reason=f"Legacy {legacy_path} should move to consumer-local {REPOSITORY_CONTEXT_PATH}.", + path=spec.path, + reason=f"Legacy {legacy_path} should move to consumer-local {spec.path}.", source_hash=None, target_hash=sha256_file(target_root / legacy_path), ) @@ -982,81 +563,9 @@ def append_repository_context_operations( operations.append( SyncOperation( action="create", - path=REPOSITORY_CONTEXT_PATH, - reason="Consumer-local repository context missing; create scaffold from the source template.", - source_hash=sha256_file(template_path), - target_hash=None, - ) - ) - - -def append_tech_operations( - source_root: Path, - target_root: Path, - operations: list[SyncOperation], - local_assets: list[str], -) -> None: - template_path = source_root / CONSUMER_LOCAL_KNOWLEDGE_TEMPLATES[TECH_PATH] - target_path = target_root / TECH_PATH - - if target_path.exists(): - local_assets.append(TECH_PATH) - operations.append( - SyncOperation( - action="preserve", - path=TECH_PATH, - reason="Preserved consumer-local technology document after scaffold materialization.", - source_hash=( - sha256_file(template_path) if template_path.exists() else None - ), - target_hash=sha256_file(target_path), - ) - ) - return - - if template_path.exists(): - operations.append( - SyncOperation( - action="create", - path=TECH_PATH, - reason="Consumer-local technology document missing; create scaffold from the source template.", - source_hash=sha256_file(template_path), - target_hash=None, - ) - ) - - -def append_structure_operations( - source_root: Path, - target_root: Path, - operations: list[SyncOperation], - local_assets: list[str], -) -> None: - template_path = source_root / CONSUMER_LOCAL_KNOWLEDGE_TEMPLATES[STRUCTURE_PATH] - target_path = target_root / STRUCTURE_PATH - - if target_path.exists(): - local_assets.append(STRUCTURE_PATH) - operations.append( - SyncOperation( - action="preserve", - path=STRUCTURE_PATH, - reason="Preserved consumer-local structure document after scaffold materialization.", - source_hash=( - sha256_file(template_path) if template_path.exists() else None - ), - target_hash=sha256_file(target_path), - ) - ) - return - - if template_path.exists(): - operations.append( - SyncOperation( - action="create", - path=STRUCTURE_PATH, - reason="Consumer-local structure document missing; create scaffold from the source template.", - source_hash=sha256_file(template_path), + path=spec.path, + reason=spec.create_reason, + source_hash=source_hash, target_hash=None, ) ) @@ -1210,7 +719,6 @@ def discover_source_sync_files(root: Path) -> set[str]: if (root / relative_path).exists() ) files.update(all_files_under(root, ".github/agents")) - files.update(all_files_under(root, ".github/instructions")) files.update(all_files_under(root, ".github/prompts")) files.update(all_files_under(root, MANAGED_SKILL_DIR)) for template_path in CONSUMER_LOCAL_KNOWLEDGE_TEMPLATES.values(): diff --git a/.github/scripts/lib/token_risks.py b/.github/scripts/lib/token_risks.py index a738318f..c078885c 100644 --- a/.github/scripts/lib/token_risks.py +++ b/.github/scripts/lib/token_risks.py @@ -1,8 +1,8 @@ from __future__ import annotations +import re from collections import defaultdict from pathlib import Path -import re import yaml @@ -25,8 +25,8 @@ DELEGATED_REVIEW_PROMPT_TOKEN_TARGET = 1100 ROOT_ALWAYS_ON_PATHS = ("AGENTS.md",) ROOT_ALWAYS_ON_TOKEN_TARGET = 4000 -COPILOT_BRIDGE_PATH = ".github/copilot-instructions.md" -COPILOT_BRIDGE_TOKEN_TARGET = 600 +COPILOT_REVIEW_PATH = ".github/copilot-instructions.md" +COPILOT_REVIEW_TOKEN_TARGET = 600 REVIEW_BASELINE_PATH = ".github/instructions/copilot-code-review.instructions.md" REVIEW_BASELINE_CHAR_LIMIT = 4000 REVIEW_BASELINE_REQUIRED_MARKERS = ( @@ -61,10 +61,10 @@ def detect_token_risks(root: Path) -> list[Finding]: findings: list[Finding] = [] - findings.extend(check_bridge_overlap(root)) + findings.extend(check_root_policy_overlap(root)) findings.extend(check_agents_operational_procedure_markers(root)) findings.extend(check_root_always_on_budget(root)) - findings.extend(check_copilot_bridge_budget(root)) + findings.extend(check_copilot_review_budget(root)) findings.extend(check_delegated_review_prompt_budget(root)) findings.extend(check_review_baseline_window(root)) findings.extend(check_inventory_dumps(root)) @@ -108,26 +108,26 @@ def check_root_always_on_budget(root: Path) -> list[Finding]: ] -def check_copilot_bridge_budget(root: Path) -> list[Finding]: - path = root / COPILOT_BRIDGE_PATH +def check_copilot_review_budget(root: Path) -> list[Finding]: + path = root / COPILOT_REVIEW_PATH if not path.exists(): return [] estimated_tokens = estimate_tokens(path) - if estimated_tokens <= COPILOT_BRIDGE_TOKEN_TARGET: + if estimated_tokens <= COPILOT_REVIEW_TOKEN_TARGET: return [] return [ Finding( severity="non-blocking", - code="copilot-bridge-over-budget", - path=COPILOT_BRIDGE_PATH, + code="copilot-review-over-budget", + path=COPILOT_REVIEW_PATH, message=( - "The Copilot bridge exceeds its compact-routing budget " - f"({estimated_tokens} estimated tokens, target {COPILOT_BRIDGE_TOKEN_TARGET})." + "The Copilot review instructions exceed their compact review budget " + f"({estimated_tokens} estimated tokens, target {COPILOT_REVIEW_TOKEN_TARGET})." ), suggestion=( - "Keep this file as a routing bridge to AGENTS.md and move policy/detail into owned instructions and skills." + "Keep this file review-only and move general policy or procedural detail into AGENTS.md, skills, or owned assets." ), ) ] @@ -239,7 +239,7 @@ def iter_skill_paths(root: Path) -> list[Path]: return sorted(path for path in skills_root.glob("**/SKILL.md") if path.is_file()) -def check_bridge_overlap(root: Path) -> list[Finding]: +def check_root_policy_overlap(root: Path) -> list[Finding]: agents_path = root / "AGENTS.md" copilot_path = root / ".github/copilot-instructions.md" if not agents_path.exists() or not copilot_path.exists(): @@ -252,13 +252,13 @@ def check_bridge_overlap(root: Path) -> list[Finding]: return [ Finding( severity="non-blocking", - code="bridge-overlap", + code="root-policy-overlap", path="AGENTS.md", message=( "AGENTS.md and .github/copilot-instructions.md share too many significant lines, " f"which increases duplicated context ({len(shared_lines)} overlapping lines)." ), - suggestion="Keep strategic bridge rules in AGENTS.md and move repo-wide Copilot behavior to .github/copilot-instructions.md.", + suggestion="Keep repository-wide agent policy in AGENTS.md and keep .github/copilot-instructions.md review-only.", ) ] @@ -279,10 +279,10 @@ def check_inventory_dumps(root: Path) -> list[Finding]: findings.append( Finding( severity="non-blocking", - code="inventory-dump-in-bridge", + code="inventory-dump-in-root-policy", path=relative_path, message="The file contains inventory-like path dumps that add noise and token cost.", - suggestion="Keep exact catalog paths in .github/INVENTORY.md instead of repeating them in bridge files.", + suggestion="Keep exact catalog paths in .github/INVENTORY.md instead of repeating them in always-on policy files.", ) ) return findings @@ -498,7 +498,7 @@ def check_internal_root_policy_overlap(root: Path) -> list[Finding]: "The internal asset repeats too much root-governance language while also citing root policy files, " f"which risks turning it into a second policy center ({len(overlap)} overlapping lines)." ), - suggestion="Keep repository-wide bridge policy in AGENTS.md plus .github/copilot-instructions.md and reduce the lower-layer asset to local scope.", + suggestion="Keep repository-wide agent policy in AGENTS.md and reduce the lower-layer asset to local scope.", ) ) return findings diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 26c8042c..b97c415e 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,8 +1,9 @@ # Dependency decision note -# Candidates: standard library only, PyYAML, python-frontmatter, pytest, tomli_w -# Final choice: PyYAML, pytest, tomli_w +# Candidates: standard library only, PyYAML, python-frontmatter, pytest, ruff, tomli_w +# Final choice: PyYAML, pytest, ruff, tomli_w # Why: PyYAML keeps frontmatter parsing explicit without a heavier content wrapper, # pytest keeps the maintenance toolkit tests concise and deterministic, +# ruff provides deterministic lint checks for script and test surfaces, # and tomli_w is required for Codex agent TOML translation in the home AI resource sync. # # This file is autogenerated by pip-compile with Python 3.13 @@ -105,5 +106,25 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via pinned toolkit input +ruff==0.15.11 \ + --hash=sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19 \ + --hash=sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4 \ + --hash=sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c \ + --hash=sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890 \ + --hash=sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7 \ + --hash=sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431 \ + --hash=sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0 \ + --hash=sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4 \ + --hash=sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3 \ + --hash=sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb \ + --hash=sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e \ + --hash=sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3 \ + --hash=sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e \ + --hash=sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5 \ + --hash=sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb \ + --hash=sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7 \ + --hash=sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33 \ + --hash=sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d + # via pinned toolkit input tomli_w==1.2.0 \ --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 diff --git a/.github/scripts/sync_copilot_catalog.py b/.github/scripts/sync_copilot_catalog.py index 3c39c8c5..baa2d5ae 100644 --- a/.github/scripts/sync_copilot_catalog.py +++ b/.github/scripts/sync_copilot_catalog.py @@ -18,7 +18,14 @@ from lib.catalog_checks import run_consistency_checks from lib.fingerprinting import HASH_ALGO, NORMALIZATION_VERSION -from lib.shared import Finding, find_repo_root, log_error, log_info, log_success, render_json +from lib.shared import ( + Finding, + find_repo_root, + log_error, + log_info, + log_success, + render_json, +) from lib.syncing import apply_sync_plan, build_sync_plan, write_sync_plan diff --git a/.github/scripts/sync_home_ai_resources.py b/.github/scripts/sync_home_ai_resources.py index a5772c3a..7ca83aea 100755 --- a/.github/scripts/sync_home_ai_resources.py +++ b/.github/scripts/sync_home_ai_resources.py @@ -2,8 +2,8 @@ """Purpose: repo-local wrapper for the bundled home AI resource sync skill script. Usage examples: - python3 ./.github/scripts/sync_home_ai_resources.py plan --targets codex,copilot,claude,opencode - python3 ./.github/scripts/sync_home_ai_resources.py apply --targets codex --create-missing-dirs + 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 diff --git a/.github/scripts/sync_home_ai_resources.sh b/.github/scripts/sync_home_ai_resources.sh index 568b3d2a..9845d669 100644 --- a/.github/scripts/sync_home_ai_resources.sh +++ b/.github/scripts/sync_home_ai_resources.sh @@ -2,8 +2,8 @@ # # Purpose: Run the local home AI resource sync planner and apply tool. # Usage examples: -# ./.github/scripts/sync_home_ai_resources.sh plan --targets codex,copilot,claude,opencode -# ./.github/scripts/sync_home_ai_resources.sh apply --targets codex --create-missing-dirs +# ./.github/scripts/sync_home_ai_resources.sh sync --targets skills --format report +# ./.github/scripts/sync_home_ai_resources.sh apply --targets codex --create-missing-dirs --format report set -Eeuo pipefail diff --git a/.github/scripts/validate_critical_output.py b/.github/scripts/validate_critical_output.py index 14d60804..2c2eebc2 100644 --- a/.github/scripts/validate_critical_output.py +++ b/.github/scripts/validate_critical_output.py @@ -14,6 +14,7 @@ import sys from pathlib import Path +from lib.cli_runner import run_finding_cli, should_fail from lib.critical_master import ( ALLOWED_CLAIM_CLASSES, ALLOWED_OUTCOMES, @@ -35,7 +36,7 @@ parse_markdown_sections, validate_outcome_value, ) -from lib.shared import log_info, log_warn, render_json +from lib.shared import log_info, log_warn def parse_args() -> argparse.Namespace: @@ -73,19 +74,13 @@ def main() -> int: else: text = sys.stdin.read() - findings = validate_output(text, max_words=args.max_words) - - if args.format == "json": - print(render_json([finding.to_dict() for finding in findings])) - elif args.format == "compact": - print(render_json(_compact_payload(findings))) - else: - render_text(findings) - - has_blocking = any(finding.severity == "blocking" for finding in findings) - if has_blocking: - return 1 - return 1 if args.strict and findings else 0 + findings = run_finding_cli( + detect_fn=lambda: validate_output(text, max_words=args.max_words), + format_name=args.format, + render_text=render_text, + compact_builder=_compact_payload, + ) + return 1 if should_fail(findings, strict=args.strict) else 0 def validate_output(text: str, *, max_words: int = TOTAL_MAX_WORDS) -> list[Finding]: diff --git a/.github/scripts/validate_internal_skills.py b/.github/scripts/validate_internal_skills.py index 5a50e327..32fe89b0 100644 --- a/.github/scripts/validate_internal_skills.py +++ b/.github/scripts/validate_internal_skills.py @@ -13,8 +13,9 @@ from collections import Counter from pathlib import Path +from lib.cli_runner import run_finding_cli, should_fail from lib.internal_skills import detect_internal_skill_findings -from lib.shared import Finding, find_repo_root, log_success, log_warn, render_json +from lib.shared import Finding, find_repo_root, log_success, log_warn def parse_args() -> argparse.Namespace: @@ -35,19 +36,15 @@ def main() -> int: args = parse_args() root = find_repo_root(Path(args.root)) selected_skills = set(args.skill) or None - findings = detect_internal_skill_findings(root, selected_skills=selected_skills) - - if args.format == "json": - print(render_json([finding.to_dict() for finding in findings])) - elif args.format == "compact": - print(render_json(build_compact_payload(findings))) - else: - render_text(findings) - - has_blocking = any(finding.severity == "blocking" for finding in findings) - if has_blocking: - return 1 - return 1 if args.strict and findings else 0 + findings = run_finding_cli( + detect_fn=lambda: detect_internal_skill_findings( + root, selected_skills=selected_skills + ), + format_name=args.format, + render_text=render_text, + compact_builder=build_compact_payload, + ) + return 1 if should_fail(findings, strict=args.strict) else 0 def render_text(findings: list[Finding]) -> None: diff --git a/.github/scripts/validate_internal_skills.sh b/.github/scripts/validate_internal_skills.sh new file mode 100755 index 00000000..96cd9903 --- /dev/null +++ b/.github/scripts/validate_internal_skills.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# +# Purpose: Validate repository-owned internal skill metadata and references. +# Usage examples: +# ./.github/scripts/validate_internal_skills.sh --root . --strict + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +exec "$SCRIPT_DIR/run.sh" validate_internal_skills "$@" diff --git a/.github/security-baseline.md b/.github/security-baseline.md index f94e9bc8..ed92640b 100644 --- a/.github/security-baseline.md +++ b/.github/security-baseline.md @@ -50,7 +50,7 @@ Provide a portable baseline that teams can apply before enabling repository-wide ## Change governance -- Document breaking changes to skills, agents, prompts, validators, or bridge policy in `.github/CHANGELOG.md`. +- Document breaking changes to skills, agents, prompts, validators, or root policy in `.github/CHANGELOG.md`. - Use a deprecation window before removing skills or agents in active use unless a documented exception applies. - Keep a rollback path for workflow and policy changes. 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 2e20daa0..be69e715 100644 --- a/.github/skills/internal-agent-support-lane-change-engine/SKILL.md +++ b/.github/skills/internal-agent-support-lane-change-engine/SKILL.md @@ -5,6 +5,14 @@ description: Use when a repository-owned internal agent needs a consistent user- # Internal Agent Support Lane Change Engine +## 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-brainstorming`: 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. + Use this skill as the shared lane-mismatch engine for repository-owned internal agents. diff --git a/.github/skills/internal-agent-support-next-step/SKILL.md b/.github/skills/internal-agent-support-next-step/SKILL.md index f322f434..64d446eb 100644 --- a/.github/skills/internal-agent-support-next-step/SKILL.md +++ b/.github/skills/internal-agent-support-next-step/SKILL.md @@ -5,6 +5,14 @@ description: Use when a repository-owned agent or prompt needs to package an alr # Internal Agent Support Next Step +## Referenced skills + +- `internal-gateway-idea-brainstorming`: 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. + Use this skill to format the next step after the next owner has already been chosen or confirmed. diff --git a/.github/skills/internal-ai-resource-review/SKILL.md b/.github/skills/internal-ai-resource-review/SKILL.md index 3c90ab07..b8b2fa26 100644 --- a/.github/skills/internal-ai-resource-review/SKILL.md +++ b/.github/skills/internal-ai-resource-review/SKILL.md @@ -82,6 +82,8 @@ 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. ## Core review rules @@ -101,14 +103,34 @@ one file. inventory, sync, validator, and test surfaces in scope before finalizing the recommendation. +## 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. + ## Output Load `references/report-contract.md` for the exact report shape. The review -output should always include: +output should always be decision-useful and include: - selected profile and target - findings or explicit keep result -- evidence labels +- 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 @@ -120,9 +142,13 @@ output should always include: - The review covers the relevant resource families for the chosen profile. - Bundle reviews include existing `references/`, `scripts/`, `assets/`, and `agents/openai.yaml`, or explicitly mark intentional non-action. +- Bundle reviews distinguish coverage from findings and compress checked-clean + surfaces into evidence digest or decision trace. - Lifecycle, compatibility, propagation, periodic review, and retirement checks are available through `references/review-checklist.md`. - `internal-copilot-audit` is used as a named drift lens when those findings are needed, not copied inline. - 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`. 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 181bc949..f594b45b 100644 --- a/.github/skills/internal-ai-resource-review/references/report-contract.md +++ b/.github/skills/internal-ai-resource-review/references/report-contract.md @@ -11,10 +11,14 @@ Use short, explicit evidence labels in findings and recommendations: - `bundle`: a bundle root plus siblings prove the claim - `validator`: a validator or script output proves the claim - `test`: a focused test proves the claim +- `test-gap`: a missing focused test is the supported risk - `sync`: sync catalog or runtime support evidence proves the claim +- `runtime-artifact`: live prompt pack, generated artifact, or fixture evidence + proves the runtime-facing behavior - `retained`: a retained package supports the claim, but live verification is still required - `gap`: evidence is missing, stale, or unverifiable +- `uncertain`: the claim remains plausible but not proven by available evidence ## Decision vocabulary @@ -43,6 +47,54 @@ Use one primary action per finding or summary line: - `retained-report`: retained output under `tmp/` only when the user asked for it or the target is already a retained package +## Adaptive layout patterns + +Use the shortest layout that still supports the user's decision. Section names +may vary, but the final answer must preserve the decision logic. + +For normal AI-resource reviews, prefer: + +1. verdict with confidence +2. material findings, defect-first +3. evidence digest +4. decision trace +5. next step +6. residual risk + +For high-severity findings, lead with findings, then impact and next step. + +For no-finding or low-finding reviews, make the decision logic explicit with a +verdict, evidence digest, decision trace, next step, and residual risk. Do not +invent extra findings to make a small review look more substantial. + +## Evidence compression + +Separate evidence into these layers: + +- `user-facing evidence`: proof the user needs to trust the verdict; +- `working evidence`: tool outputs, broad grep results, long diffs, file maps, + and checklists used during review; +- `internal synthesis`: comparisons, grouping, counterfactuals, and rejected + paths. + +Only user-facing evidence appears by default. Compress working evidence with an +evidence digest, a 2-4 line decision trace, compact evidence labels, and a named +residual risk. Use small tables only when they clarify the decision. Suppress raw +output when a digest preserves the same proof strength. + +## Missing proof handling + +When a proof cannot be run or loaded, report: + +1. the unavailable proof; +2. why it could not be used; +3. the confidence impact; +4. the substitute check used, if any; +5. the expected follow-up validation. + +Partial evidence may support a verdict, but the unavailable focused proof stays +visible as residual risk. + ## Required sections Every final review should include: @@ -54,6 +106,9 @@ Every final review should include: 5. validation path or explicit evidence gap 6. residual risk +Small reviews may merge sections only when the verdict, evidence digest, +decision trace, next action, and residual risk remain understandable. + ## Completeness pass Before closing the review, confirm all of these: @@ -65,5 +120,7 @@ Before closing the review, confirm all of these: 4. Bundle reviews confirm each existing bundle sibling was reviewed or marked intentional non-action. 5. Drift findings came from `internal-copilot-audit` when that lens was needed. -6. The report stays proportional to the evidence and does not expand into an +6. No-finding and low-finding reviews still explain why the result matters and + what next action follows. +7. The report stays proportional to the evidence and does not expand into an encyclopedic catalog dump. diff --git a/.github/skills/internal-ai-resource-review/references/review-profiles.md b/.github/skills/internal-ai-resource-review/references/review-profiles.md index 7a806fb1..49f9bae9 100644 --- a/.github/skills/internal-ai-resource-review/references/review-profiles.md +++ b/.github/skills/internal-ai-resource-review/references/review-profiles.md @@ -61,6 +61,12 @@ in-scope owner: bundle points to. 4. Confirm propagation surfaces such as inventory, sync, runtime matrices, or explicit allowlists. +5. Check live prompt packs, generated artifacts, retained reports, or fixtures + when the bundle governs materialized output. + +Report coverage separately from findings. Checked-clean siblings and propagation +surfaces should support the verdict through an evidence digest or decision trace +instead of appearing as artificial findings. ### `catalog` 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 new file mode 100644 index 00000000..4de71d60 --- /dev/null +++ b/.github/skills/internal-ai-resource-review/references/review-usefulness-replay-fixture.md @@ -0,0 +1,63 @@ +# Review Usefulness Replay Fixture + +Use this fixture when validating that AI-resource reviews are decision-useful +without becoming long reports. + +## Input + +- Target: the `local-ai-chatgpt-prompt-creator` skill bundle behavior. +- Branch diff tightens only the `coach-personale` validator profile. +- Live `coach-personale` prompt pack passes the validator with the required + support pack. +- No focused `coach-personale` profile test exists. +- Focused pytest execution is unavailable in the active environment. +- Sync and inventory evidence do not show material drift. + +## Expected Review Behavior + +- Reports no immediate runtime break observed. +- Reports one material low-severity finding or decision note about missing + profile-specific tests. +- Explains why the low finding matters. +- Uses an evidence digest instead of raw command output. +- Includes a decision trace that rules out unsupported drift or runtime-break + claims. +- Names unavailable pytest execution as residual risk. +- Recommends the smallest useful next step: add focused `coach-personale` + pass/fail validator tests. +- Does not invent additional findings to make the review look more substantial. + +## Compressed Output Shape + +```markdown +## Verdict + +No immediate runtime break observed. The live `coach-personale` pack passes the +updated validator, but the tightened profile lacks dedicated regression +coverage. + +## Findings + +Low: `coach-personale` profile rules changed without a focused pass/fail test. + +## Evidence Digest + +Checked branch diff, validator helper, live prompt pack, nearby tests, and +inventory/sync references. + +## Decision Trace + +Runtime break is not supported because the live pack passes. Sync drift is not +supported because the changed surface is the validator helper and catalog +references remain present. The supported risk is test protection for the new +profile contract. + +## Next Step + +Add focused `coach-personale` validator tests for the required tokens and +support files. + +## Residual Risk + +Focused pytest execution was unavailable in the active environment. +``` diff --git a/.github/skills/internal-aws-governance/SKILL.md b/.github/skills/internal-aws-governance/SKILL.md index 479f2d91..92814b60 100644 --- a/.github/skills/internal-aws-governance/SKILL.md +++ b/.github/skills/internal-aws-governance/SKILL.md @@ -5,6 +5,13 @@ description: Use when the user needs AWS governance guidance for IAM operating m # Internal AWS Governance +## Referenced skills + +- `internal-aws-strategic`: route back when direction or tradeoff framing is still unsettled. +- `internal-aws-organization-structure`: route when account, OU, delegated admin, or topology structure is the main decision. +- `internal-aws-operations`: route when rollout validation, monitoring, or evidence is the main need. +- `internal-aws-mcp-research`: load when current AWS IAM or service documentation can change the answer. + Use this skill when the next need is to define or review AWS identity, access, and guardrail decisions. This skill owns governance logic after the broad structure is known. It helps separate org-level guardrails from account-level access design and keeps permission decisions auditable. diff --git a/.github/skills/internal-aws-mcp-research/SKILL.md b/.github/skills/internal-aws-mcp-research/SKILL.md index f79e9967..0c1699cf 100644 --- a/.github/skills/internal-aws-mcp-research/SKILL.md +++ b/.github/skills/internal-aws-mcp-research/SKILL.md @@ -5,6 +5,13 @@ description: Use when the task needs current AWS documentation or safe IAM inspe # Internal AWS MCP Research +## Referenced skills + +- `internal-aws-strategic`: route recommendations that affect platform direction or tradeoffs. +- `internal-aws-organization-structure`: route recommendations that affect account, OU, delegated admin, or StackSets topology. +- `internal-aws-governance`: route recommendations that affect SCPs, IAM, trust, or federation. +- `internal-aws-operations`: route recommendations that affect validation, monitoring, backup, or rollout evidence. + Use this skill when AWS decisions depend on up-to-date documentation or on safe inspection of real IAM state. ## When to use diff --git a/.github/skills/internal-aws-operations/SKILL.md b/.github/skills/internal-aws-operations/SKILL.md index 484f0a40..d989a162 100644 --- a/.github/skills/internal-aws-operations/SKILL.md +++ b/.github/skills/internal-aws-operations/SKILL.md @@ -5,6 +5,13 @@ description: Use when the user needs AWS operational guidance for monitoring, lo # Internal AWS Operations +## Referenced skills + +- `internal-aws-strategic`: route back when the platform direction is still unsettled. +- `internal-aws-organization-structure`: route when account, OU, delegated admin, or topology structure is the main decision. +- `internal-aws-governance`: route when IAM, trust, SCP, or guardrail design is the main decision. +- `internal-aws-mcp-research`: load when current AWS service behavior or IAM inspection can change the validation plan. + Use this skill when the next need is to validate, observe, or operationalize an AWS platform decision. This skill owns the operational side of the platform: monitoring, evidence, preflight, and post-rollout verification. It does not replace strategic framing, structure design, or governance design. diff --git a/.github/skills/internal-aws-organization-structure/SKILL.md b/.github/skills/internal-aws-organization-structure/SKILL.md index a3ee9747..db35daa4 100644 --- a/.github/skills/internal-aws-organization-structure/SKILL.md +++ b/.github/skills/internal-aws-organization-structure/SKILL.md @@ -5,6 +5,13 @@ description: Use when the user needs AWS control-plane or multi-account structur # Internal AWS Organization Structure +## Referenced skills + +- `internal-aws-strategic`: route back when the platform direction or tradeoff frame is still unsettled. +- `internal-aws-governance`: route guardrail, IAM, SCP, trust, or federation design. +- `internal-aws-operations`: route monitoring, validation, backup, reporting, or post-rollout evidence. +- `internal-aws-mcp-research`: load when current AWS service support or delegated-admin behavior can change the structure answer. + Use this skill when the next need is to design or review how AWS is structured at organization and platform level. This skill owns AWS layout decisions, not generic strategy and not detailed IAM or monitoring implementation. It helps translate a platform goal into account, OU, delegated admin, network, and rollout structure. diff --git a/.github/skills/internal-aws-strategic/SKILL.md b/.github/skills/internal-aws-strategic/SKILL.md index 2e22f24a..3fd9df36 100644 --- a/.github/skills/internal-aws-strategic/SKILL.md +++ b/.github/skills/internal-aws-strategic/SKILL.md @@ -5,6 +5,17 @@ description: Use when the user needs high-level AWS platform decision support or # Internal AWS Strategic +## Referenced skills + +- `antigravity-aws-cost-optimizer`: cost-analysis depth when AWS cost data or recommendations are primary. +- `internal-aws-organization-structure`: route when account, OU, delegated admin, or topology structure becomes the decision. +- `internal-aws-governance`: route when IAM, trust, SCP, or guardrail design becomes the decision. +- `internal-aws-operations`: route when validation, monitoring, backup, or audit evidence becomes the decision. +- `internal-aws-mcp-research`: load when current AWS documentation or service behavior can change the recommendation. +- `internal-terraform`: route implementation work in Terraform or OpenTofu. +- `internal-python-script`: route implementation work in standalone Python automation. +- `internal-bash-script`: route implementation work in standalone Bash automation. + Use this skill when the main need is to reason about an AWS decision before implementation. This is a strategic support skill. It helps frame the decision, compare realistic options, expose tradeoffs, and recommend a direction. It does not implement the change and it does not choose Terraform, Python, or Bash on behalf of the user. diff --git a/.github/skills/internal-azure-governance/SKILL.md b/.github/skills/internal-azure-governance/SKILL.md index 051604b4..d9825627 100644 --- a/.github/skills/internal-azure-governance/SKILL.md +++ b/.github/skills/internal-azure-governance/SKILL.md @@ -5,6 +5,13 @@ description: Use when the user needs Azure governance guidance for RBAC operatin # Internal Azure Governance +## Referenced skills + +- `internal-azure-strategic`: route back when direction or tradeoff framing is still unsettled. +- `internal-azure-organization-structure`: route when tenant, management-group, subscription, or landing-zone layout is the main decision. +- `internal-azure-operations`: route when rollout validation, monitoring, backup, or evidence is the main need. +- `awesome-copilot-azure-role-selector`: least-privilege RBAC role selection depth. + Use this skill when the next need is to define or review Azure identity, access, and guardrail decisions. This skill owns governance logic after the broad structure is known. It helps separate tenant or management-group guardrails from subscription or workload grants and keeps permission decisions auditable. diff --git a/.github/skills/internal-azure-operations/SKILL.md b/.github/skills/internal-azure-operations/SKILL.md index 5b3397b0..c5cf78ab 100644 --- a/.github/skills/internal-azure-operations/SKILL.md +++ b/.github/skills/internal-azure-operations/SKILL.md @@ -5,6 +5,13 @@ description: Use when the user needs Azure operational guidance for monitoring, # Internal Azure Operations +## Referenced skills + +- `internal-azure-strategic`: route back when direction or tradeoff framing is still unsettled. +- `internal-azure-organization-structure`: route when tenant, management-group, subscription, or landing-zone layout is the main decision. +- `internal-azure-governance`: route when RBAC, managed identity, PIM, Policy, or guardrail design is the main decision. +- `awesome-copilot-azure-resource-health-diagnose`: Azure resource-health diagnosis depth. + Use this skill when the next need is to validate, observe, or operationalize an Azure platform decision. This skill owns the operational side of the platform: monitoring, evidence, preflight, and post-rollout verification. It does not replace strategic framing, structure design, or governance design. diff --git a/.github/skills/internal-azure-organization-structure/SKILL.md b/.github/skills/internal-azure-organization-structure/SKILL.md index 77464ab9..0f11ee0a 100644 --- a/.github/skills/internal-azure-organization-structure/SKILL.md +++ b/.github/skills/internal-azure-organization-structure/SKILL.md @@ -5,6 +5,12 @@ description: Use when the user needs Azure control-plane or platform-structure g # Internal Azure Organization Structure +## Referenced skills + +- `internal-azure-strategic`: route back when direction or tradeoff framing is still unsettled. +- `internal-azure-governance`: route RBAC, managed identity, PIM, Policy, tagging, or guardrail design. +- `internal-azure-operations`: route monitoring, validation, backup, Site Recovery, reporting, or audit evidence. + Use this skill when the next need is to design or review how Azure is structured at tenant and platform level. This skill owns Azure layout decisions, not generic strategy and not detailed RBAC or monitoring implementation. It helps translate a platform goal into management-group, subscription, landing-zone, topology, and rollout structure. diff --git a/.github/skills/internal-azure-strategic/SKILL.md b/.github/skills/internal-azure-strategic/SKILL.md index 1263a40e..596a8dec 100644 --- a/.github/skills/internal-azure-strategic/SKILL.md +++ b/.github/skills/internal-azure-strategic/SKILL.md @@ -5,6 +5,16 @@ description: Use when the user needs high-level Azure platform decision support # Internal Azure Strategic +## Referenced skills + +- `awesome-copilot-azure-pricing`: pricing depth when Azure cost data can change the decision. +- `internal-azure-organization-structure`: route when tenant, management-group, subscription, or landing-zone layout becomes the decision. +- `internal-azure-governance`: route when RBAC, identity, Policy, tagging, or guardrail design becomes the decision. +- `internal-azure-operations`: route when validation, monitoring, backup, or audit evidence becomes the decision. +- `internal-terraform`: route implementation work in Terraform or OpenTofu. +- `internal-python-script`: route implementation work in standalone Python automation. +- `internal-bash-script`: route implementation work in standalone Bash automation. + Use this skill when the main need is to reason about an Azure decision before implementation. This is a strategic support skill. It helps frame the decision, compare realistic options, expose tradeoffs, and recommend a direction. It does not implement the change and it does not choose Terraform, Python, or Bash on behalf of the user. diff --git a/.github/skills/internal-bash-script/SKILL.md b/.github/skills/internal-bash-script/SKILL.md index f79afd25..7a8aeab3 100644 --- a/.github/skills/internal-bash-script/SKILL.md +++ b/.github/skills/internal-bash-script/SKILL.md @@ -5,6 +5,11 @@ description: Use when creating or modifying standalone Bash scripts or shell uti # Bash Script Skill +## Referenced skills + +- `internal-github-action-composite`: route Bash embedded in GitHub composite actions. +- `internal-github-actions`: route GitHub workflow-level behavior. + ## When to use - New Bash scripts. diff --git a/.github/skills/internal-changelog-automation/SKILL.md b/.github/skills/internal-changelog-automation/SKILL.md index f5d08461..477a8927 100644 --- a/.github/skills/internal-changelog-automation/SKILL.md +++ b/.github/skills/internal-changelog-automation/SKILL.md @@ -5,6 +5,10 @@ description: Use when creating or improving changelog generation, release-note w # Internal Changelog Automation +## Referenced skills + +- None. + Use this skill when the repository needs deterministic release notes instead of ad hoc summaries. This skill is guidance for designing or improving changelog automation. It does not imply that the current repository already has an active changelog generator wired into CI. diff --git a/.github/skills/internal-cloud-policy/SKILL.md b/.github/skills/internal-cloud-policy/SKILL.md index 32906a5b..e9f07564 100644 --- a/.github/skills/internal-cloud-policy/SKILL.md +++ b/.github/skills/internal-cloud-policy/SKILL.md @@ -5,6 +5,10 @@ description: Use when authoring, comparing, or reviewing concrete cloud policy d # Cloud Policy Skill +## Referenced skills + +- None. + ## When to use - Create or modify governance policy definitions (AWS SCP, Azure Policy, GCP Org Policy). diff --git a/.github/skills/internal-copilot-audit/SKILL.md b/.github/skills/internal-copilot-audit/SKILL.md index 9e97f139..04bf3a7d 100644 --- a/.github/skills/internal-copilot-audit/SKILL.md +++ b/.github/skills/internal-copilot-audit/SKILL.md @@ -5,6 +5,10 @@ description: Use when auditing repository-owned GitHub Copilot assets for overla # Internal Copilot Audit +## Referenced skills + +- None. + Use this skill when auditing the health of the Copilot customization catalog. This skill is often invoked by planning or sync lanes, but it can also be used directly for a focused repository audit when the user explicitly wants overlap, stale-reference, or governance-drift findings. diff --git a/.github/skills/internal-copilot-docs-research/SKILL.md b/.github/skills/internal-copilot-docs-research/SKILL.md index e5618aa8..bc6c54c5 100644 --- a/.github/skills/internal-copilot-docs-research/SKILL.md +++ b/.github/skills/internal-copilot-docs-research/SKILL.md @@ -5,6 +5,10 @@ description: Use when a `.github/` customization change depends on current GitHu # Internal Copilot Docs Research +## Referenced skills + +- None. + Use this skill when a repository customization decision depends on current GitHub Copilot behavior rather than repo-local convention alone. ## When to use @@ -67,7 +71,7 @@ Do not assume MCP is configured just because GitHub Copilot supports it. - Use skills for detailed, reusable task workflows that should load only when relevant. - Use agents for recurring orchestration roles with stable routing boundaries. - Use MCP for live tools, external context, or server-backed workflows only when the current session actually exposes the needed capability. -- When researching custom agents, treat `tools:` as supported, not deprecated. GitHub Copilot allows omitted `tools:`, but this repository's internal-agent policy requires an explicit `tools:` contract for repository-owned internal agents. +- When researching custom agents, treat `tools:` as supported, not deprecated. GitHub Copilot allows omitted `tools:`, but this repository's internal agent policy requires an explicit `tools:` contract for repository-owned internal agents. - Prefer canonical tool aliases such as `read`, `edit`, `search`, `execute`, `agent`, and `web` over legacy product-specific tool ids from older examples. - Treat `infer:` as retired. Use `disable-model-invocation` and `user-invocable` when selection behavior must be constrained. - Treat `mcp-servers:` as GitHub.com and Copilot CLI configuration, not as an IDE-wide guarantee. diff --git a/.github/skills/internal-devops-core-principles/SKILL.md b/.github/skills/internal-devops-core-principles/SKILL.md index 974c990f..002021fe 100644 --- a/.github/skills/internal-devops-core-principles/SKILL.md +++ b/.github/skills/internal-devops-core-principles/SKILL.md @@ -5,6 +5,10 @@ description: Use when the task is about delivery-system strategy, release safety # Internal DevOps Core Principles +## Referenced skills + +- None. + Use this skill for end-to-end delivery-system thinking, not for one-off YAML syntax questions or provider-specific command lookup. ## When to use diff --git a/.github/skills/internal-docker/SKILL.md b/.github/skills/internal-docker/SKILL.md index a5979e19..a9a9c60b 100644 --- a/.github/skills/internal-docker/SKILL.md +++ b/.github/skills/internal-docker/SKILL.md @@ -5,6 +5,10 @@ description: Use when creating or modifying Dockerfiles, Compose assets, image b # Docker Skill +## Referenced skills + +- None. + ## When to use - Creating or updating `Dockerfile` assets. diff --git a/.github/skills/internal-excel/SKILL.md b/.github/skills/internal-excel/SKILL.md new file mode 100644 index 00000000..60efe124 --- /dev/null +++ b/.github/skills/internal-excel/SKILL.md @@ -0,0 +1,93 @@ +--- +name: internal-excel +description: Use when tasks involve CSV, TSV, or Excel tabular data profiling, schema validation, cleanup, joins, conversions, optimization, or large-file integrity. +--- + +# Internal Excel + +## Referenced skills + +- `openai-spreadsheet`: on-demand support owner when rendered fidelity, cached recalculation, charts, or polished workbook presentation are the primary requirement. + +## When to use + +- CSV, TSV, or `.xlsx` tasks centered on tabular data quality, profiling, schema choices, normalization, joins, dedupe, reconciliation, conversion, or large-file processing. +- Requests where the main decision is tool selection for scale, memory use, or integrity tradeoffs across local data files. +- Workbook extraction, contract inspection, writer parity checks, or value-level updates where rendered presentation fidelity is not the primary requirement. +- Requests to copy a workbook tab contract into generated tabs: column order, widths, header or body style, money style, alert or error row style, and formula columns. +- Requests where derived workbook columns must stay as Excel formulas and raw source fields remain atomic input values. + +## When not to use + +- Charts, rendered review, cached recalculation, or polished workbook presentation as a first-class delivery requirement; use `openai-spreadsheet`. +- Single-language implementation work after the tabular-data approach is already chosen; use the narrower file or runtime owner for that code. +- Database or warehouse design work that is not primarily about local CSV, TSV, or Excel artifacts. + +## Boundary + +- This skill owns data integrity first: headers, types, nulls, duplicates, joins, reconciliation, stable IDs, delimiter detection, encoding, locale-sensitive numeric fields, and row-count preservation. +- Keep evidence compact for large files: report headers, counts, targeted anomalies, transformation rules, exact validation gaps, and any locale or coercion assumptions that change numeric meaning. +- Treat `.xlsx` as a workbook container first. Stay here for tabular extraction, safe value-level transformation, workbook contract inspection, writer parity checks, and formula-column decisions. Route to `openai-spreadsheet` only when rendered fidelity, cached recalculation, charts, or presentation-preserving delivery is the main job. +- Preserve identifier fidelity before coercion: keep leading zeros, long numeric IDs, and day-first date text exact until an explicit schema rule says otherwise. +- Flag spreadsheet-bound text that could execute as a formula and minimize sensitive-column exposure in samples or logs. +- Read `references/tool-selection.md` when choosing between Python `csv`, `pandas`, `openpyxl`, PyArrow, Polars, or DuckDB, or when scale, memory use, file format, or workbook fidelity makes the engine choice non-obvious. +- Read `references/data-integrity-and-safety.md` when delimiter or encoding detection, locale coercion, operation-specific proof, formula injection, or sensitive data exposure are material. +- Read `references/large-file-token-discipline.md` when file size, broad profiling, repeated output, sampling strategy, or context pressure could make the tabular workflow expensive. + +## Noise Exclusions + +- In discovery commands, exclude `.venv`, `__pycache__`, `.pytest_cache`, dependency directories, generated outputs, and binary exports that are not the active evidence target. +- Ignore workbook lock files such as `~$*` and `.~lock.*`. +- Prefer targeted `rg` queries on source files, writer modules, and the named workbook path. Do not start with broad repo scans when the target file or owner path is already known. + +## Workbook Token Gate + +- Before broad `.xlsx` reads, collect only bounded metadata: file size, sheet names, target-sheet dimensions, target headers, essential row counts, formula count, and style or style-id counts. +- Do not dump workbook XML, full worksheets, broad profiler output, or whole tables into chat unless they are the missing evidence. +- Read only the target sheets, columns, ranges, or writer blocks needed for the active question. +- Before opening large code modules, use `rg` to find owner functions, workbook writers, style builders, and formula helpers, then read only the needed blocks. + +## Workbook Contracts + +- When a sample workbook or sample tab defines the expected layout, treat that tab as a contract: column order, widths, header style, body style, money style, alert or error row style, and formula columns. +- Verify that every tab produced by the same writer receives the same contract where applicable, not just the sampled tab. +- When the user asks for verifiable formulas, keep derived columns as Excel formulas and keep source columns as atomic input values instead of precomputed results. +- If workbook behavior stays contract-level and formula-level, keep the guidance here. Route to `openai-spreadsheet` only when visual polish, charts, or recalculated delivery artifacts become primary. + +## Binary Artifacts + +- Do not modify sample workbooks, generated exports, or other binary artifacts unless the user explicitly asks for that edit. +- If output validation needs a generated workbook, write it to `tmp/` or another declared controlled path and report that path. +- Do not include Excel or LibreOffice lock files in discovery, validation, diffs, or deliverables. + +## Runtime And Validation + +- If the local toolchain or bundle exposes a `.venv` or declared runtime, use that runtime for `py_compile`, tests, or `pytest` instead of ambient Python. +- Run the smallest deterministic validation that proves the workbook or tabular change; keep output bounded and artifact-oriented. + +## Token Discipline + +- Run a token budget gate before inspecting large or unknown-size files: capture file size, format, sheet names or delimiter, cheap row and column counts, headers, encoding clues, and the smallest representative sample needed. +- Do not paste full tables, raw workbook XML, full profiling JSON, or broad command output into chat by default. Report bounded summaries, counts, schema, anomalies, transformation rules, artifact paths, and validation gaps. +- Treat row samples as discovery only. Keep samples small and redacted, then prove transformations with full-file aggregate checks such as counts, key scans, coercion failures, or reconciliation queries. +- Prefer streaming reads, column projection, chunked scans, DuckDB, Polars, or PyArrow for large files. Use full `pandas` loads or full workbook traversal only after size and scope show that the token and memory cost is justified. +- For `.xlsx`, inspect workbook metadata first and read only the target sheets or ranges when possible. Do not escalate to broad workbook traversal unless the metadata gate shows it is necessary. +- Pause before expensive expansions such as dumping more rows, profiling every sheet, loading all columns, or running repeated wide joins. If the user explicitly asks for full output, state the context impact and provide the smallest bounded next slice or a local artifact path. + +## Workflow + +1. Confirm the artifact type, row scale, token budget risk, whether workbook fidelity matters, and whether locale-specific numeric conventions or spreadsheet reopening are in play. +2. Pick the narrowest tool that preserves the needed integrity and performance. +3. Inspect headers, sample rows, counts, null patterns, key columns, and locale-sensitive numeric fields before broad transforms. +4. For `.xlsx`, inspect metadata and the active workbook contract before broad reads: sheet names, target dimensions, headers, formula counts, style counts, and the relevant writer blocks. +5. Apply deterministic transformations with explicit schema rules for IDs, dates, currency, and decimal text, and keep a reconciliation path for row counts, keys, duplicates, dropped records, and formula-column intent. +6. Re-run focused integrity and safety checks after each material transform or workbook-writer change. +7. Hand off only when rendered workbook polish, charts, cached recalculation, or presentation-preserving delivery becomes the main requirement. + +## Validation + +- Run the smallest deterministic check that proves the transform: row counts, schema diff, duplicate-key scan, delimiter or encoding confirmation, locale-sensitive numeric confirmation, join cardinality, or a reconciliation sample. +- For large files, validation output must stay compact: full-file checks should produce aggregate evidence, anomaly counts, sampled examples only when needed, and paths to generated reports instead of inline dumps. +- When data may return to spreadsheet tools, verify formula-injection handling and avoid exposing raw sensitive values in samples or validation output. +- Run `python3 ./.github/scripts/validate_internal_skills.py --skill internal-excel --strict` after editing this skill bundle. +- Run the closest inventory consistency check when registering or renaming the skill. diff --git a/.github/skills/internal-excel/agents/openai.yaml b/.github/skills/internal-excel/agents/openai.yaml new file mode 100644 index 00000000..2770b088 --- /dev/null +++ b/.github/skills/internal-excel/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Internal Excel" + short_description: "Tabular data profiling and cleanup" + default_prompt: "Use $internal-excel for CSV, TSV, or Excel tabular data profiling, schema checks, cleanup, conversions, and large-file transformations." diff --git a/.github/skills/internal-excel/references/data-integrity-and-safety.md b/.github/skills/internal-excel/references/data-integrity-and-safety.md new file mode 100644 index 00000000..9dfea524 --- /dev/null +++ b/.github/skills/internal-excel/references/data-integrity-and-safety.md @@ -0,0 +1,32 @@ +# Data Integrity And Safety + +Use this reference when tabular work risks corrupting meaning, dropping records, leaking sensitive data, or creating spreadsheet-executable output. + +## Locale and Excel pitfalls + +- Default CSV handling may stay comma-delimited on macOS and Linux, but detect semicolon-delimited or mixed-locale exports instead of assuming them. +- Treat decimal commas, thousands separators, currency symbols, and percentages as schema decisions; prove the chosen normalization rule before coercion. +- Preserve string semantics for identifiers such as CAPs, codes, account numbers, and long numeric IDs when leading zeros or more than 15 digits matter. +- Verify day-first dates and Excel auto-converted values against protected source samples before normalizing them. +- Check encoding early when accented text, smart quotes, or office exports suggest UTF-8 with BOM or Windows-1252. +- Prefer explicit schema declarations for IDs, dates, currency, and nullable numeric fields when inference could corrupt meaning. +- Preserve original delimiters, quoting, newline style, and column order unless the task explicitly changes them. + +## Integrity checks by operation + +| Operation | Minimum proof | +| --- | --- | +| Filter or dedupe | Input and output row counts plus kept vs dropped rationale | +| Join or merge | Join cardinality, unmatched keys, and duplicate-key scan | +| Type coercion | Before and after schema, coercion failures, and null increase | +| Locale-sensitive numeric normalization | Raw samples, chosen decimal or date rule, coercion failures, and proof that protected IDs kept string fidelity | +| Conversion between CSV, TSV, XLSX, and Parquet | Row counts plus header equality or a mapped rename log | +| Stable ID generation | Collision scan and deterministic rule | +| Spreadsheet-bound export | Formula-injection scan or escaping rule plus row-count parity | +| Large-file sampling | Sampling is discovery only; full-file validation still applies to transforms | + +## Security and sensitive data + +- If CSV or TSV output will be reopened in Excel or LibreOffice, neutralize or flag untrusted text starting with `=`, `+`, `-`, or `@`. +- Keep PII and confidential fields out of logs, chat excerpts, and validation samples unless the task explicitly requires those raw values. +- Prefer hashed, truncated, or representative-key reconciliation when you need to prove joins or duplicates without exposing full identifiers. diff --git a/.github/skills/internal-excel/references/large-file-token-discipline.md b/.github/skills/internal-excel/references/large-file-token-discipline.md new file mode 100644 index 00000000..0ceb1ea0 --- /dev/null +++ b/.github/skills/internal-excel/references/large-file-token-discipline.md @@ -0,0 +1,27 @@ +# Large-File Token Discipline + +Use this reference when CSV, TSV, `.xlsx`, or exported worksheet work could create large prompts, repeated tool output, expensive profiling, or memory-heavy local processing. + +## Gate + +- Start with metadata and bounded evidence: file size, format, worksheet names, dimensions when cheap, delimiter or encoding clues, headers, row counts, and column counts. +- Decide whether the next action is discovery, transformation, or proof. Do not use discovery samples as completion evidence. +- Name the cost checkpoint before expanding to all rows, all sheets, all columns, broad profiling, or repeated wide joins. + +## Output posture + +- Avoid full content reads, full profiler output, raw workbook XML, and unbounded terminal output in chat. +- Save detailed reports as local artifacts and summarize only counts, schema changes, anomalies, transformation rules, artifact paths, and validation gaps. +- Keep samples small, representative, and redacted. Use hashed, truncated, or representative-key examples when raw identifiers are sensitive. + +## Large-file handling + +- Project only needed columns and target only needed sheets before sampling, profiling, joining, or converting. +- Prefer streaming reads, chunked scans, DuckDB, Polars, or PyArrow when a full in-memory load is unnecessary or risky. +- For `.xlsx`, inspect workbook metadata first and avoid full workbook traversal unless workbook scope proves it is needed. + +## Proof + +- Use deterministic full-file aggregate checks for proof: row counts, schema diffs, duplicate-key scans, unmatched-key counts, coercion failures, null increases, and reconciliation totals. +- For multi-file joins, report join cardinality, unmatched-key counts, duplicate-key counts, and only the smallest safe examples needed to diagnose anomalies. +- If the user asks for full output, state the context impact and provide the smallest bounded next slice or a local artifact path. diff --git a/.github/skills/internal-excel/references/tool-selection.md b/.github/skills/internal-excel/references/tool-selection.md new file mode 100644 index 00000000..d0a87656 --- /dev/null +++ b/.github/skills/internal-excel/references/tool-selection.md @@ -0,0 +1,33 @@ +# Tool Selection + +Read this reference when scale, file format, or workbook fidelity makes the engine choice non-obvious. + +## Quick routing + +| Situation | Preferred tool | Why | Notes | +| --- | --- | --- | --- | +| Header sniffing, delimiter detection, exact raw CSV pass | Python `csv` | Minimal dependency and exact text handling | Good for first-pass validation and small streaming transforms. | +| Small or medium CSV or TSV analysis, schema cleanup, reshaping | `pandas` | Familiar transform surface | Prefer when the file fits memory and workbook fidelity is irrelevant. | +| Large local files, multi-file joins, aggregations, SQL-style reconciliation | DuckDB | Fast scans and joins without full in-memory loads | Good for `.csv`, `.tsv`, Parquet, and exported worksheet tables. | +| Columnar scans, type-stable interchange, Parquet or Arrow conversion | PyArrow | Strong schema control and efficient IO | Prefer when conversion fidelity and explicit types matter. | +| Large in-memory transforms with strict schema and speed focus | Polars | Fast columnar execution | Prefer when `pandas` becomes memory-heavy or slow. | +| Reading or updating `.xlsx` cell values while workbook UX is secondary | `openpyxl` | Native workbook access | Stay here only when layout, styles, and rendered review are not the main goal. | +| Formulas, styles, charts, cached recalculation, rendered review, workbook preservation | `openai-spreadsheet` | Workbook UX owner | Route out of this skill. | + +## Selection rules + +- Start with the smallest tool that can prove or preserve the needed integrity. +- Do not default to `pandas` for files that only need sniffing, counts, or a simple streaming pass. +- Do not use `openpyxl` as a general data-frame replacement; it is slower for heavy tabular transforms and does not evaluate formulas. +- Prefer DuckDB, Polars, or PyArrow when file size or join volume makes full `pandas` loads risky. +- Prefer engines that support explicit schema declarations when inference could corrupt IDs, dates, currency, or nullable numeric fields. + +## Escalation to `openai-spreadsheet` + +Route to `openai-spreadsheet` when any of these are first-class requirements: + +- formulas or cached recalculation +- charts or workbook layout +- preserving styles, comments, merged cells, or sheet presentation +- rendered review of spreadsheet output +- creating a polished workbook for user delivery diff --git a/.github/skills/internal-gateway-critical-master/SKILL.md b/.github/skills/internal-gateway-critical-master/SKILL.md index 7e96b62c..d58f95f9 100644 --- a/.github/skills/internal-gateway-critical-master/SKILL.md +++ b/.github/skills/internal-gateway-critical-master/SKILL.md @@ -5,6 +5,13 @@ description: Use when a repository-owned plan, proposal, or decision needs a cri # Internal Gateway Critical Master +## Referenced skills + +- `internal-gateway-idea-brainstorming`: route when critique shows the plan must be reformulated. +- `internal-gateway-simple-task`: route when critique leaves a concrete local task. +- `internal-gateway-execute-plans`: route approved retained-plan execution. +- `internal-gateway-review`: route when the next risk is correctness evidence. + Use this skill as the portable core for critical challenge work. The calling gateway decides when to invoke it; this skill challenges only. diff --git a/.github/skills/internal-gateway-idea-brainstorming/SKILL.md b/.github/skills/internal-gateway-idea-brainstorming/SKILL.md index 9f04ddf8..708135bf 100644 --- a/.github/skills/internal-gateway-idea-brainstorming/SKILL.md +++ b/.github/skills/internal-gateway-idea-brainstorming/SKILL.md @@ -12,8 +12,11 @@ an on-demand dependency index. Do not preload them; load only the owner proved by the active uncertainty or next checkpoint. - `grill-me`: guided decision interview. +- `internal-gateway-simple-task`: direct execution owner after a confident handoff when retained planning is not recommended. +- `internal-gateway-review`: defect-first review owner. - `internal-gateway-critical-master`: mandatory critical challenge owner. - `internal-gateway-writing-plans`: retained-plan authoring after a confident critical outcome. +- `internal-gateway-execute-plans`: approved retained-plan execution owner. - `internal-agent-support-next-step`: next-step package formatting. Portable skill-first idea gateway. This skill now owns substantive idea work @@ -40,7 +43,8 @@ through retained-plan creation. It stops before execution. - This gateway is not a specialized execution owner. A concrete task may not be accepted for execution here until Idea Gate 0 is `grill-me satisfied` and `Critical Gate 2` is `confident`. - For a direct concrete operation, emit `Specialization Checkpoint: gated`, explain that this owner cannot decide task ownership or execute yet, and continue with the bounded evidence pass plus mandatory `grill-me`. - User insistence does not bypass Idea Gate 0 or Critical Gate 2. -- Only after `Critical Gate 2: confident` may this gateway ask whether the user wants this owner to stay in charge of the task; before that question, do not execute edits, commands, or operational steps. +- Only after `Critical Gate 2: confident` may this gateway present the direct execution vs retained plan recommendation; before that point, do not execute edits, commands, operational steps, or owner handoffs. +- After `Critical Gate 2: confident`, the recommendation must name the concrete consequence: specialized direct execution or retained-plan authoring. - Do not run critical automatically after convergence; ask the user whether to continue. - Do not create a retained plan automatically after a confident critical outcome; require explicit `go`/`ok`/`procedi` or equivalent approval. - Use `internal-gateway-critical-master` before finalizing any substantive definition. @@ -58,8 +62,8 @@ through retained-plan creation. It stops before execution. State rules: - If the incoming request is already concrete (file edit, command execution, validator run, or implementation step), start with `Specialization Checkpoint: gated` before Idea Gate 0. -- At `Specialization Checkpoint: gated`, name the recommended specialized owner (`internal-gateway-simple-task` by default, `internal-gateway-review` for defect-first review, `internal-gateway-critical-master` for pressure testing), but do not ask the user to keep this owner yet. -- Continue through the bounded evidence pass, mandatory `grill-me`, and critical gate before asking whether this owner should stay in charge of the task. +- At `Specialization Checkpoint: gated`, name the recommended specialized owner (`internal-gateway-simple-task` by default, `internal-gateway-review` for defect-first review, `internal-gateway-critical-master` for pressure testing), but do not present the post-critical decision yet. +- Continue through the bounded evidence pass, mandatory `grill-me`, and critical gate before presenting the direct execution vs retained plan recommendation. - Before the initial numbered block, keep large-file and large-log evidence compact: summarize counts, headers, anomalies, routes, and open gaps unless raw content is itself the missing evidence. - After the evidence pass, load `grill-me` and ask one mandatory numbered bulk question block with recommendations and defaults. - Before the initial numbered block, emit a compact facts/options summary derived from the bounded evidence pass. @@ -67,10 +71,42 @@ State rules: - Declare `Interview Gate 1: ready-for-critical` only when material branches are resolved, assumptions/defaults are visible and accepted, no ledger contradictions remain, and the validation path is identified. - At `Interview Gate 1: ready-for-critical`, ask whether to continue before loading `internal-gateway-critical-master`. - `Critical Gate 2` outcomes are: targeted reopen of affected branches, continue-critical, or confident completion. -- At `Critical Gate 2: confident`, ask whether the user wants this owner to keep the task; only if they say yes should this gateway ask for explicit plan approval before loading `internal-gateway-writing-plans`. +- At `Critical Gate 2: confident`, emit the `Direct Execution vs Retained Plan Recommendation` before any handoff or plan approval question. - Alias mapping is fixed: `mini-plan` means `compact` and `plan` means `extended`; retained-plan execution strategy is inferred by `internal-gateway-execute-plans`. - At `Handoff Gate 4: plan-created`, set `Continuation: waiting` and do not execute. +## Direct Execution vs Retained Plan Recommendation + +Use this recommendation only after `Critical Gate 2: confident`. + +Recommend direct execution via `internal-gateway-simple-task` when the scoped +work is concrete, low-to-medium risk, one owner, one lane, one primary +validation path, no cross-turn dependency, and no material context-pressure +signal. Make clear that direct execution still belongs to the specialized +owner; this gateway does not execute it. + +Recommend a retained plan when the user explicitly asked for a plan, the work +needs more than roughly 5-7 executable steps, touches more than roughly three +unrelated path families, has multiple independent validators, depends on +external approvals or pins, carries material context-pressure risk, or must stop +before execution. Recommend `compact` only when one owner, one lane, and one +validation path remain enough; otherwise recommend `extended`. + +The user-facing message must use this shape: + +1. `Recommendation:` direct execution via `internal-gateway-simple-task`, or a + `compact`/`extended` retained plan. +2. `Why:` one evidence-based sentence naming the decisive signals. +3. `Tradeoff:` direct execution means same-chat specialized work after approval; + retained plan means a durable handoff under `tmp/superpowers/` and no + execution in this conversation. +4. `Decision:` ask the user to choose `execute`, `plan`, or an explicit + override. If `plan` is chosen, require `go`/`ok`/`procedi` or equivalent + approval before loading `internal-gateway-writing-plans`. + +Avoid vague owner-retention phrasing. The message must name the concrete +consequence: direct execution handoff or retained-plan authoring. + ## Flow 1. Specialization checkpoint @@ -78,7 +114,7 @@ State rules: 3. Converge 4. Ask before critical 5. Critical -6. Ownership confirmation +6. Direct execution vs retained plan recommendation 7. Plan approval 8. Plan creation 9. Stop before execution @@ -86,7 +122,7 @@ State rules: ## Validation - The gateway keeps `idea -> critical -> retained plan` in one conversation. -- Concrete execution requests trigger the specialization checkpoint and do not execute, transfer ownership, or ask to keep this owner until `grill-me` and critical both pass. +- Concrete execution requests trigger the specialization checkpoint and do not execute, transfer ownership, or present the post-critical recommendation until `grill-me` and critical both pass. - User insistence does not bypass the `grill-me` or critical gates. - `internal-gateway-writing-plans` owns profile selection. - Execution stays a manual boundary after plan creation. diff --git a/.github/skills/internal-gateway-idea-brainstorming/agents/openai.yaml b/.github/skills/internal-gateway-idea-brainstorming/agents/openai.yaml index f4ea7f77..10cd568a 100644 --- a/.github/skills/internal-gateway-idea-brainstorming/agents/openai.yaml +++ b/.github/skills/internal-gateway-idea-brainstorming/agents/openai.yaml @@ -23,20 +23,26 @@ interface: numbered question block with recommendations and rationale. Treat visible defaults as accepted unless the user overrides them by number. Ask focused follow-up blocks only for unresolved, dependent, or reopened branches. Do - not converge, recommend simple-task, recommend planning, hand off, or ask - whether this owner should keep the task until Idea Gate 0 is grill-me - satisfied and Critical Gate 2 is confident. After convergence, emit + not converge, recommend simple-task, recommend planning, hand off, or + present the Direct Execution vs Retained Plan Recommendation until Idea + Gate 0 is grill-me satisfied and Critical Gate 2 is confident. After convergence, emit Interview Gate 1: ready-for-critical only when material branches are resolved, assumptions/defaults are accepted, no ledger contradictions remain, and the validation path is identified. At Interview Gate 1: ready-for-critical, ask whether to continue before loading $internal-gateway-critical-master. In Critical Gate 2, either reopen targeted branches for another focused grill-me block, continue critical, or conclude Critical Gate 2: confident. After Critical Gate 2: confident, - ask whether the user wants this owner to keep the task. Only if the user - says yes, emit Plan Approval Gate 3: waiting and ask for explicit - go/ok/procedi or equivalent approval before loading - $internal-gateway-writing-plans; otherwise route to the recommended - specialized owner and stop. Alias mapping is fixed: mini-plan means + emit a Direct Execution vs Retained Plan Recommendation with four visible + fields: Recommendation, Why, Tradeoff, and Decision. Recommend direct + execution via internal-gateway-simple-task when the work is concrete, one + owner, one lane, one validation path, and low context risk. Recommend a + compact or extended retained plan when the user asked for one, the work is + broad enough to retain, validation is multi-surface, or context pressure + could interrupt verified execution. Avoid vague owner-retention phrasing. + Ask the user to choose execute, plan, or an explicit override. Only if the user chooses plan, + emit Plan Approval Gate 3: waiting and ask for explicit go/ok/procedi or + equivalent approval before loading $internal-gateway-writing-plans; + otherwise route to the recommended specialized owner and stop. Alias mapping is fixed: mini-plan means compact and plan means extended; retained-plan execution strategy is inferred later by internal-gateway-execute-plans. After plan creation, emit Handoff Gate 4: plan-created with Continuation: waiting and stop diff --git a/.github/skills/internal-gateway-idea-brainstorming/references/guided-decision-interview.md b/.github/skills/internal-gateway-idea-brainstorming/references/guided-decision-interview.md index fc3d89b1..05b913fa 100644 --- a/.github/skills/internal-gateway-idea-brainstorming/references/guided-decision-interview.md +++ b/.github/skills/internal-gateway-idea-brainstorming/references/guided-decision-interview.md @@ -19,7 +19,8 @@ Use this reference when `internal-gateway-idea-brainstorming` needs the exact pa 1. After all material branches resolve and Idea Gate 0 is `grill-me satisfied`, summarize the compact decision ledger and declare `Interview Gate 1: ready-for-critical` only when assumptions/defaults are accepted, no ledger contradictions remain, and the validation path is identified. 1. At `Interview Gate 1: ready-for-critical`, ask whether to continue to critical before loading `internal-gateway-critical-master`. 1. When a reopen occurs, resume only the affected branches unless the impact is broad, and declare `Interview Gate 1: reopen`. -1. After `Critical Gate 2: confident`, ask whether the user wants this owner to keep the task. Only if the user says yes, declare `Plan Approval Gate 3: waiting` and ask for explicit `go`/`ok`/`procedi` or equivalent approval before loading `internal-gateway-writing-plans`. +1. After `Critical Gate 2: confident`, emit the `Direct Execution vs Retained Plan Recommendation`: `Recommendation`, `Why`, `Tradeoff`, and `Decision`. Recommend direct execution via `internal-gateway-simple-task` when the work is concrete, one owner, one lane, one validation path, and low context risk. Recommend a `compact` or `extended` retained plan when the user asked for one, the work is broad enough to retain, validation is multi-surface, or context pressure could interrupt verified execution. +1. Avoid vague owner-retention phrasing. The user chooses `execute` for specialized direct execution, `plan` for retained-plan authoring, or an explicit override. Only if the user chooses `plan`, declare `Plan Approval Gate 3: waiting` and ask for explicit `go`/`ok`/`procedi` or equivalent approval before loading `internal-gateway-writing-plans`. 1. Only after explicit approval, declare `Plan Approval Gate 3: approved`, create the retained plan, then declare `Handoff Gate 4: plan-created`. 1. Treat `Continuation: waiting` as a handoff lock after `Handoff Gate 4: plan-created`; proposals, alternatives, or wording preferences do not clear the lock without explicit owner/action/scope approval. @@ -46,9 +47,9 @@ Each row in the decision ledger: | `Idea Gate 0: grill-me satisfied` | User answered or explicitly accepted defaults for the current request, scope, context, and evidence. | Before convergence or any simple-task/planning recommendation. | | `Interview Gate 1: ready-for-critical` | All material branches resolved; waiting for critical challenge. | After decision-ledger summary and before loading `internal-gateway-critical-master`. | | `Interview Gate 1: reopen` | Critical pass or realignment reopened one or more branches. | When resuming affected branches after `reopen`. | -| `Critical Gate 2: confident` | Critical challenge found the Definition Brief fit for handoff and unlocked the owner-choice question. | Before asking whether this owner should keep the task. | +| `Critical Gate 2: confident` | Critical challenge found the Definition Brief fit for handoff and unlocked the direct execution vs retained plan recommendation. | Before asking the user to choose `execute`, `plan`, or an explicit override. | | `Critical Gate 2: reopen` | Critical challenge found material issues. | Before returning to affected branches in `discover`. | -| `Plan Approval Gate 3: waiting` | Critical is confident, the user kept this owner, and planning approval is pending. | Immediately after `Critical Gate 2: confident` and after the user keeps this owner. | +| `Plan Approval Gate 3: waiting` | Critical is confident, the user chose retained-plan authoring, and planning approval is pending. | Immediately after `Critical Gate 2: confident` and after the user chooses `plan`. | | `Plan Approval Gate 3: approved` | Explicit plan approval was received. | After explicit `go`/`ok`/`procedi` or equivalent approval. | | `Handoff Gate 4: plan-created` | Retained plan was created and execution is blocked. | After plan creation with `Continuation: waiting` and stop-before-execution behavior. | diff --git a/.github/skills/internal-gateway-idea-brainstorming/scripts/audit_contract.py b/.github/skills/internal-gateway-idea-brainstorming/scripts/audit_contract.py index 0aaa33ea..20bb3db1 100644 --- a/.github/skills/internal-gateway-idea-brainstorming/scripts/audit_contract.py +++ b/.github/skills/internal-gateway-idea-brainstorming/scripts/audit_contract.py @@ -37,9 +37,12 @@ def main() -> int: "ask_before_critical": "ask whether to continue" in skill_text and "ask whether to continue" in reference_text and "ask whether to continue" in runtime_text, - "ownership_after_critical": "ask whether the user wants this owner to keep the task" in skill_text - and "ask whether the user wants this owner to keep the task" in reference_text - and "ask whether the user wants this owner to keep the task" in runtime_text, + "direct_vs_plan_recommendation": "Direct Execution vs Retained Plan Recommendation" in skill_text + and "Direct Execution vs Retained Plan Recommendation" in reference_text + and "Direct Execution vs Retained Plan Recommendation" in runtime_text + and "choose `execute`, `plan`, or an explicit" in skill_text + and "chooses `execute`" in reference_text + and "choose execute, plan, or an explicit override" in runtime_text, "explicit_plan_approval": "go`/`ok`/`procedi" in skill_text and "go`/`ok`/`procedi" in reference_text and "go/ok/procedi" in runtime_text, diff --git a/.github/skills/internal-gateway-review/SKILL.md b/.github/skills/internal-gateway-review/SKILL.md index 63d60e60..b34d973c 100644 --- a/.github/skills/internal-gateway-review/SKILL.md +++ b/.github/skills/internal-gateway-review/SKILL.md @@ -22,9 +22,10 @@ Portable review orchestrator. Owns review scope, lens selection, findings consolidation, critical support, and remediation-plan transition. It does not apply fixes. Before any user-visible verdict, run a lightweight internal check for evidence, -severity, false positives, contrary evidence, and scope narrowing. Load -`internal-gateway-critical-master` only for a material challenge. Revise or -reopen when the check exposes a material gap. +severity, false positives, contrary evidence, scope narrowing, and decision +usefulness. Load `internal-gateway-critical-master` only for a material +challenge. Revise or reopen when the check exposes a material gap or when the +visible review would not support a clear next decision. See `references/review-gate.md` for the review output contract and gate states. @@ -83,7 +84,8 @@ gateway does not choose the execution owner. - Lens selection matches the changed-path families; AI customization assets route to `internal-ai-resource-review` (drift via `internal-copilot-audit`) instead of being skipped by the code lens. - Review flow preserves compact context: prioritize diff and failing evidence first, then expand only when an evidence gap remains. - Large evidence may be reported compactly, but each material finding still keeps severity, confidence, evidence gap, counter-validation result, and route or next owner. -- Review output carries findings, severity, confidence, evidence gap, counter-validation result, route or next owner, and a Review Gate outcome before the final verdict. +- Review output carries findings, severity, confidence, evidence gap, counter-validation result, route or next owner, decision-usefulness result, and a Review Gate outcome before the final verdict. +- Low-finding and no-finding reviews include enough evidence digest, decision trace, next action, and residual-risk context for the reader to decide whether to accept, patch, investigate, plan, or accept with named risk. - The review cannot present analysis to the user until counter-validation confirms it or reopens material gaps. - Remediation-plan transitions preserve a 100% material-finding coverage map or explicitly declare a `partial remediation plan`. - Retained remediation plans are authored by `internal-gateway-writing-plans` and preserve the coverage map. diff --git a/.github/skills/internal-gateway-review/references/review-gate.md b/.github/skills/internal-gateway-review/references/review-gate.md index 5c0c4f7e..e8aaecb6 100644 --- a/.github/skills/internal-gateway-review/references/review-gate.md +++ b/.github/skills/internal-gateway-review/references/review-gate.md @@ -10,12 +10,33 @@ Use this reference when `internal-gateway-review` needs to package findings befo - Evidence gap - Counter-validation - Route or next owner +- Decision-usefulness check - Review Gate outcome +## Decision-Usefulness Check + +Before the final user-visible verdict, confirm that the review supports a clear +decision: accept, patch, investigate, plan, or accept with a named residual +risk. + +The reader should understand: + +- the verdict and confidence; +- the smallest evidence set that supports it; +- what was ruled out or not supported by evidence; +- why any finding, keep result, or no-finding result matters; +- the best next action; +- the most important residual risk. + +If any material part is missing, use `review gate: reopen` until the missing +decision context is added. Strong findings may carry most of this context. No +findings and low-severity-only reviews need explicit coverage, decision trace, +and residual-risk context. + ## Gate States - `review gate: satisfied` when the findings are specific, routed, counter-validated, and ready for the user-visible verdict. -- `review gate: reopen` when material evidence is missing, counter-validation exposes a material flaw, or the remediation choice needs more challenge. +- `review gate: reopen` when material evidence is missing, counter-validation exposes a material flaw, the remediation choice needs more challenge, or the visible review is formally correct but too thin to support a clear user decision. ## Boundary diff --git a/.github/skills/internal-gateway-simple-task/SKILL.md b/.github/skills/internal-gateway-simple-task/SKILL.md index dc8c2873..87a425d2 100644 --- a/.github/skills/internal-gateway-simple-task/SKILL.md +++ b/.github/skills/internal-gateway-simple-task/SKILL.md @@ -22,6 +22,11 @@ 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 @@ -35,6 +40,9 @@ 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. ## When to use @@ -55,6 +63,9 @@ 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. @@ -67,6 +78,7 @@ Classify every simple task before operational work as `full-gate`, 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`. @@ -75,6 +87,9 @@ Classify every simple task before operational work as `full-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 @@ -113,7 +128,8 @@ Classify every simple task before operational work as `full-gate`, 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. + 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. @@ -123,19 +139,24 @@ Classify every simple task before operational work as `full-gate`, 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. Identify mandatory applicable requirements internally before execution; do - not emit a default user checklist. -14. Execute the one concrete lane with the Agentic Execution Loop, or, in +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`, write the retained plan and stop before execution. -15. Run focused validation or name the explicit gap. -16. Run a pre-close compliance audit over mandatory applicable requirements +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`. -17. Block completion claims when mandatory applicable requirements remain +19. Run `Direct Completion Control` before any `DONE`, readiness, fixed, or + complete claim. +20. Block completion claims when mandatory applicable requirements remain unverified. -18. If architecture ownership, owner conflicts, or validation strategy are +21. If architecture ownership, owner conflicts, or validation strategy are ambiguous, escalate instead of assuming a universal rule. -19. If the task stops being simple, stop and issue an escalation alert. +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. @@ -144,64 +165,65 @@ Escalation trigger: if evidence collection, ownership checks, or validation need When execution is already authorized, stay inside the active owner, target scope, anti-scope, and validation path, then iterate: -1. Confirm the current goal and nearest evidence. +1. Confirm the current goal, nearest evidence, and the open direct-control item. 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`, a blocker, or an explicit evidence gap. +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. -## Plan Mode +## Direct Execution Control -Plan mode lets a concrete simple task produce a retained plan instead of -executing in the same chat. The task stays single-lane and single-phase; only -the output shape changes. +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. -### Activation +Track these fields for non-trivial direct execution: -- **Mandatory explicit**: if the user asks for a plan with keywords such as - `plan`, `piano`, `modalità plan`, `retained plan`, `scrivi il piano`, - `non eseguire ancora`, or similar, switch to plan mode and honor the request. -- **Implicit cost signal**: if the user says nothing about a plan, but cost - signals show that same-chat execution would be less economical than a retained - plan, declare `plan-mode` and ask the user to confirm before writing the plan. - See `references/plan-mode.md` for the exact cost-signal checklist. +- 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 -### Profile - -- Default to `compact` (`tmp/superpowers/mini-plan-*`) with - `01-change-summary.md` and `02-execution.md` only when the task stays within - one owner, one execution lane, one primary validation path, and low - completeness risk. -- Apply the `Plan Profile Selection Guard` before proposing `compact`. -- Use `extended` when the task needs multi-slice execution, multiple - independent validators, an articulated anti-scope, external pins, - cross-skill token-discipline work, validator-impacting changes, or - exports, generated reports, and datasets that need non-trivial - reconciliation. - -### Procedure - -1. Classify the gate as `plan-mode`. -2. Run `grill-me` and `internal-gateway-critical-master` exactly as for - `full-gate`. -3. Load `internal-gateway-writing-plans` and author the retained plan using its - local contract. -4. Stop before execution. Report the plan folder and next owner - (`internal-gateway-execute-plans`). - -### Boundaries - -- Do not use plan mode to avoid a hard ownership decision. If the target, - anti-scope, or validation strategy is ambiguous, `escalate` instead. -- Do not use plan mode for vague ideas or substantive tradeoffs; use - `internal-gateway-idea-brainstorming`. -- Do not execute the plan inside simple task. Execution belongs to - `internal-gateway-execute-plans`. +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, profile, confirmation, procedure, +boundary, and example rules. After `grill-me` and critical review, load +`internal-gateway-writing-plans` and hand off execution to +`internal-gateway-execute-plans`. ## Deterministic Helpers @@ -221,24 +243,23 @@ the output shape changes. - `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 instead of checking applicability. -- Skipping `grill-me` and the critical gate without a Trivial-skip proof. -- Treating `full`, `idea`, or `complete` as advisory when the user meant to force the full gate. -- Switching to plan mode without declaring it or without user confirmation on implicit cost signals. -- Executing the plan inside simple task instead of handing off to `internal-gateway-execute-plans`. -- Expanding the Readiness Brief into a long checklist or proceeding without explicit user approval when no narrower auto-execute exception applies. -- Treating a generic `next_action.allowed=true` value as enough for auto-execution without checking the narrower skill's stop conditions. -- Continuing the Agentic Execution Loop after evidence stops improving or a - stop condition fires. -- Declaring completion after code edits while mandatory applicable evidence is still missing. -- Promoting specialist requirements to universal policy without target/runtime ownership proof. -- Continuing without escalation when ownership conflicts or validation strategy remain undefined. +- 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. diff --git a/.github/skills/internal-gateway-simple-task/agents/openai.yaml b/.github/skills/internal-gateway-simple-task/agents/openai.yaml index 47363fe1..d3e03f64 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, execution, diagnosis, validation, or plan authoring. Switch to plan mode when the user asks for a plan or when same-chat execution would be less economical than a retained plan, producing a compact or extended retained plan 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 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, producing a compact or extended retained plan instead of executing. Route approved retained-plan execution to internal-gateway-execute-plans." 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 6d0eef88..1a0856a3 100644 --- a/.github/skills/internal-gateway-simple-task/references/simple-lanes.md +++ b/.github/skills/internal-gateway-simple-task/references/simple-lanes.md @@ -37,14 +37,14 @@ lane or output shape still needs a quick decision. For `answer`, return the answer, evidence, and uncertainty. -For `edit`, return `lane`, `support-loaded`, `files-touched`, focused -validation, and residual risk. +For `edit`, return `lane`, `support-loaded`, `files-touched`, direct-control +status, focused validation, and residual risk. For `diagnose`, return `lane`, `support-loaded`, the reproduced failure, root -cause, fix or blocker, and evidence. +cause, direct-control status, fix or blocker, and evidence. For `validate`, return `lane`, `support-loaded`, the command or check, result, -and any follow-up owner. +direct-control status, and any follow-up owner. For `escalate`, return the boundary break, next owner, scope, action, validation path, and risk. 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 bd9a704b..1c97770f 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 @@ -42,12 +42,21 @@ "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.", }, + DIRECT_COMPLETION_REQUIREMENT, { "owner": "superpowers-verification-before-completion", "evidence_gate": "Fresh validation evidence, not intent or stale output.", @@ -58,6 +67,7 @@ "owner": "internal-tdd", "evidence_gate": "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.", @@ -68,6 +78,7 @@ "owner": "internal-performance-optimization", "evidence_gate": "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.", @@ -78,6 +89,7 @@ "owner": "internal-github-pr", "evidence_gate": "Check PR lifecycle evidence before the claim.", }, + DIRECT_COMPLETION_REQUIREMENT, { "owner": "superpowers-verification-before-completion", "evidence_gate": "Fresh validation evidence, not intent or stale output.", @@ -104,18 +116,21 @@ }, ], "completion": [ + DIRECT_COMPLETION_REQUIREMENT, { "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, { "owner": "superpowers-verification-before-completion", "evidence_gate": "Re-run the validator and read fresh output before the claim.", diff --git a/.github/skills/internal-gateway-writing-plans/SKILL.md b/.github/skills/internal-gateway-writing-plans/SKILL.md index 4063ff2b..1e469262 100644 --- a/.github/skills/internal-gateway-writing-plans/SKILL.md +++ b/.github/skills/internal-gateway-writing-plans/SKILL.md @@ -80,6 +80,15 @@ path sufficient despite lower-context execution. coverage, blockers, or implementation-contract detail in `01-change-summary.md`. Put those facts in `02-execution.md` for `compact` or `02-control.md` for `extended`. +- `01-change-summary.md` must still preserve `counter-validation-critical facts`: + observable result criteria without which the user cannot verify whether the + plan really covers the request. These are result facts, not execution detail. + Keep them in `Risultato atteso` or `Risorse coinvolte`. +- Do not compress away concrete contract facts that materially define the + requested result, such as column order, new columns, required fields, + must-never-be-empty conditions, blocking diagnostics, repaired data gaps, + row-count or no-data-loss invariants, or user-visible output-contract + changes. - Compact plans have a 2,000 estimated-token total budget measured as `ceil(UTF-8 bytes / 4)` across plan Markdown files. Keep `02-execution.md` under 1,500 estimated tokens. Treat warnings as required review inputs. @@ -97,6 +106,17 @@ path sufficient despite lower-context execution. in chat or request context; do not create another retained file for it. - `01-change-summary.md`: compressed Italian decision capsule following `Explicit Constraints`. +- Distinguish execution detail from `counter-validation-critical facts`. + Execution detail covers route, commands, source-item ids, file-by-file steps, + fallback logic, and validator order; those stay out of `01-change-summary.md`. + `Counter-validation-critical facts` are the user-observable end-state facts + needed to verify coverage at a glance, and they stay in `Risultato atteso` + or `Risorse coinvolte`. +- When the plan changes output, schema, or data behavior, do not collapse the + result into generic phrases such as `rispetta il nuovo schema`, `aggiorna gli + output`, or `corregge i dati` if the request depends on concrete facts like + first columns, field order, newly added columns, never-empty fields, + blocking diagnostics, repaired row ranges, or row-count invariants. - `02-execution.md` for `compact` must include these exact headings: `Plan profile`, `Target and anti-scope` (with `### Target` and `### Anti-scope`), `Owner and validator`, `Stop conditions`, `Objective`, @@ -133,7 +153,9 @@ path sufficient despite lower-context execution. 2. For debugging, drift, or data-mismatch work, write the diagnosis capsule before `01-change-summary.md`. 3. For new plans, run bundle-local `init` first to create the scaffold. -4. Choose folder name and write the compressed `01-change-summary.md` in Italian. +4. Choose folder name and write the compressed `01-change-summary.md` in Italian, + preserving the user-visible `counter-validation-critical facts` needed to + verify coverage without reading the control file. 5. For `compact`, write `02-execution.md` with profile, control header, steps, and source-item coverage. 6. For `extended`, write `02-control.md` with profile, control facts, merged @@ -156,6 +178,9 @@ path sufficient despite lower-context execution. - Plan lives under `tmp/superpowers//`. - `01-change-summary.md` is Italian, compressed, and contains only the required decision-capsule sections. +- `01-change-summary.md` preserves the most important + `counter-validation-critical facts` in `Risultato atteso` or + `Risorse coinvolte` when the plan changes output, schema, or data behavior. - `compact` uses `01-change-summary.md` + `02-execution.md` only. - `compact` stays under the 2,000 estimated-token total budget, or the warning was resolved by compression or escalation. @@ -173,4 +198,7 @@ path sufficient despite lower-context execution. - Skipping the diagnosis capsule and turning an unproven mismatch into a retained plan. - Using `compact` when execution actually needs an implementation contract. +- Compressing away user-visible contract facts from `01-change-summary.md` and + leaving only generic schema or output wording that prevents + counter-validation. - Creating `done-*` markers during authoring. diff --git a/.github/skills/internal-gateway-writing-plans/references/compact-plan-contract.md b/.github/skills/internal-gateway-writing-plans/references/compact-plan-contract.md index 4752f869..eeb7f48b 100644 --- a/.github/skills/internal-gateway-writing-plans/references/compact-plan-contract.md +++ b/.github/skills/internal-gateway-writing-plans/references/compact-plan-contract.md @@ -90,6 +90,24 @@ Do not put chosen logic, validation detail, execution route notes, source-item coverage, blockers, or implementation-contract detail in `01-change-summary.md`. Those facts belong in `02-execution.md`. +`01-change-summary.md` must still preserve `counter-validation-critical facts`: +observable result criteria without which the user cannot counter-validate +whether the plan really covers the request. These facts are not execution +detail. Keep them in `Risultato atteso` or `Risorse coinvolte`. + +Keep concrete examples when they materially define the requested result, such +as column order, newly added columns, required fields, must-never-be-empty +conditions, blocking diagnostics, repaired data gaps, row-count or no-data-loss +invariants, or changed user-visible output contracts. + +Do not move execution detail into the summary: commands, file-by-file steps, +validator order, source-item ids, fallback logic, and coverage mechanics stay +in `02-execution.md`. + +A generic line like `rispetta il nuovo schema`, `aggiorna gli output`, or +`corregge i dati` is insufficient when the source-item coverage tracks concrete +schema, output, or data requirements that the user needs to verify at a glance. + ## Template: 02-execution.md Required sections: diff --git a/.github/skills/internal-gateway-writing-plans/references/plan-review-gate.md b/.github/skills/internal-gateway-writing-plans/references/plan-review-gate.md index d3da491e..0c095899 100644 --- a/.github/skills/internal-gateway-writing-plans/references/plan-review-gate.md +++ b/.github/skills/internal-gateway-writing-plans/references/plan-review-gate.md @@ -23,6 +23,7 @@ or handoff. It checks clarity and validity without creating reviewer personas. | Scope | Are anti-scope and stop conditions explicit enough to prevent drift? | | Summary focus | Does `01-change-summary.md` contain only the compressed Italian decision capsule with required sections and a `Risorsa \| Azione \| Scopo` table for non-trivial plans? | | Summary clarity | Are changed resources and intended actions obvious in the summary? | +| Summary counter-validation | Does `01-change-summary.md` preserve enough observable result criteria for the user to counter-validate coverage without reading `02-execution.md` or `02-control.md`? | | Semantic coverage | For `compact`, does `02-execution.md` preserve each requested or source item with stable item id, observable acceptance, evidence class, status, and route? For `extended`, does `02-control.md` preserve the same coverage? | | Implementation contract | For `extended` profiles, does `02-control.md` include the exact sources, target files, validation order, blockers, and any external pin or fallback? For `compact` profiles, a separate implementation contract is not required. | | Profile | Is `Plan profile` declared as `compact` or `extended`? Missing or unsupported profiles return `unsupported-plan-contract`. | @@ -60,6 +61,10 @@ or handoff. It checks clarity and validity without creating reviewer personas. - Treat a long or overloaded `01-change-summary.md` as a plan-quality defect, not as a documentation nit. It should be a compressed decision capsule, not a control file. - Treat an English `01-change-summary.md` as a plan-quality defect per the current retained-plan contract (Italian only). - Treat an unclear resource table or missing resource-action-purpose columns in `01-change-summary.md` as a plan-quality defect. +- Treat a summary as a plan-quality defect when output, schema, or data-contract + changes are compressed into generic formulas without concrete observable + examples. Keep those user-visible facts in `Risultato atteso` or + `Risorse coinvolte`, not in execution detail. - Treat a missing or weak source-item ledger as a plan-quality defect, not as a documentation nit. - Treat a missing evidence pass or reading budget as a token-waste defect for non-trivial retained plans. - Treat missing source-item coverage for requested work as a plan-quality defect, not as an editorial preference. diff --git a/.github/skills/internal-gateway-writing-plans/references/scope-challenge.md b/.github/skills/internal-gateway-writing-plans/references/scope-challenge.md index 7c6c1aa9..d8be9a08 100644 --- a/.github/skills/internal-gateway-writing-plans/references/scope-challenge.md +++ b/.github/skills/internal-gateway-writing-plans/references/scope-challenge.md @@ -28,9 +28,10 @@ Every retained plan must be able to answer these questions: 9. `profile`: Is `Plan profile` declared as `compact` or `extended`? Currently, no other profiles are supported. Missing or unrecognized profiles return `unsupported-plan-contract`. 10. `summary language`: Is `01-change-summary.md` written in Italian as a compressed decision capsule with required sections and a `Risorsa | Azione | Scopo` table for non-trivial plans? 11. `summary clarity`: Are changed resources and intended actions obvious in the summary without reading deeper files? -12. `route map`: Do `Source item ledger` routes point to existing numbered files or explicit non-action routes? -13. `questions state`: Is `questions.md` either `- none` (ready) or explicitly blocking execution handoff? -14. `token budget`: For `compact`, is total plan Markdown within 2,000 estimated tokens, with `01-change-summary.md` under 300 and `02-execution.md` under 1,500? +12. `summary counter-validation`: Does the summary contain enough observable criteria to let the user counter-validate the plan without reading the control file? +13. `route map`: Do `Source item ledger` routes point to existing numbered files or explicit non-action routes? +14. `questions state`: Is `questions.md` either `- none` (ready) or explicitly blocking execution handoff? +15. `token budget`: For `compact`, is total plan Markdown within 2,000 estimated tokens, with `01-change-summary.md` under 300 and `02-execution.md` under 1,500? For non-trivial retained plans and strategic-to-operational or monolithic-to-executable conversions, also answer this: @@ -79,6 +80,7 @@ Stop conditions: Profile: Summary language: Summary clarity: +Summary counter-validation: Source-item ledger: Reading budget: Observable acceptance: diff --git a/.github/skills/internal-gateway-writing-plans/scripts/plan_authoring.py b/.github/skills/internal-gateway-writing-plans/scripts/plan_authoring.py index 6fc4b05a..90b17137 100644 --- a/.github/skills/internal-gateway-writing-plans/scripts/plan_authoring.py +++ b/.github/skills/internal-gateway-writing-plans/scripts/plan_authoring.py @@ -53,6 +53,35 @@ "External pins", ) NON_ACTION_ROUTES = frozenset({"closed", "manual", "gap", "not applicable", "n/a", "none"}) +GENERIC_SUMMARY_PATTERNS = ( + re.compile(r"\brispett\w+\s+(?:il|lo|la)?\s*nuov\w+\s+schema\b", re.IGNORECASE), + re.compile(r"\baggiorn\w+\s+gli?\s+output\b", re.IGNORECASE), + re.compile(r"\bcorregg\w+\s+i\s+dati\b", re.IGNORECASE), + re.compile(r"\balline\w+\s+(?:gli?\s+output|lo\s+schema|i\s+dati)\b", re.IGNORECASE), + re.compile(r"\badatt\w+\s+(?:gli?\s+output|lo\s+schema|i\s+dati)\b", re.IGNORECASE), +) +OBSERVABLE_CONTRACT_PATTERNS = { + "column-order": re.compile( + r"\b(column|columns|colonn\w*|order|ordine|first|before|prime?\s+colonne|prima\s+di)\b", + re.IGNORECASE, + ), + "required-field": re.compile( + r"\b(required|mandatory|obbligator\w*|must\s+never\s+be\s+empty|never\s+be\s+empty|sempre\s+valorizzat\w*|mai\s+vuot\w*|non\s+vuot\w*|always\s+populated)\b", + re.IGNORECASE, + ), + "diagnostic": re.compile( + r"\b(diagnostic\w*|bloccant\w*|blocking|must\s+fail|fails?\b|errore|errori|error)\b", + re.IGNORECASE, + ), + "data-integrity": re.compile( + r"\b(gap|missing|rows?\b|righe\b|row[- ]count|data[- ]loss|perdita\s+dati|nessuna\s+perdita|buco\s+dati)\b", + re.IGNORECASE, + ), + "contract-output": re.compile(r"\b(schema|output|contract|generated|field|fields|colum\w*)\b", re.IGNORECASE), +} +IDENTIFIER_SIGNAL_RE = re.compile(r"\b[a-z][a-z0-9]*_[a-z0-9_]+\b") +NUMERIC_RANGE_RE = re.compile(r"\b\d+\s*-\s*\d+\b") +NUMERIC_SIGNAL_RE = re.compile(r"\b\d+\b") @dataclass @@ -186,6 +215,98 @@ def _validate_summary(summary_text: str) -> list[Finding]: return findings +def _summary_focus_text(summary_text: str) -> str: + parts = [ + _section_body(summary_text, "Risultato atteso"), + _section_body(summary_text, "Risorse coinvolte"), + ] + return "\n".join(part for part in parts if part).strip() + + +def _identifier_signals(text: str) -> set[str]: + return {match.lower() for match in IDENTIFIER_SIGNAL_RE.findall(text)} + + +def _observable_contract_categories(text: str) -> set[str]: + return { + name + for name, pattern in OBSERVABLE_CONTRACT_PATTERNS.items() + if pattern.search(text) + } + + +def _coverage_text(plan_folder: Path, profile: str) -> str: + if profile == "compact": + path = plan_folder / "02-execution.md" + heading = "Source item coverage" + else: + path = plan_folder / "02-control.md" + heading = "Source item ledger" + if not path.is_file(): + return "" + return _section_body(path.read_text(encoding="utf-8"), heading) + + +def _has_observable_contract_requirements(coverage_text: str) -> bool: + if not coverage_text: + return False + categories = _observable_contract_categories(coverage_text) + specific_categories = categories - {"contract-output"} + if _identifier_signals(coverage_text): + return True + if len(specific_categories) >= 2: + return True + return bool(specific_categories and "contract-output" in categories) + + +def _has_concrete_counter_validation_examples(summary_focus: str, coverage_text: str) -> bool: + coverage_identifiers = _identifier_signals(coverage_text) + summary_identifiers = _identifier_signals(summary_focus) + if coverage_identifiers & summary_identifiers: + return True + + summary_categories = _observable_contract_categories(summary_focus) - {"contract-output"} + if len(summary_categories) >= 2: + return True + if summary_categories and NUMERIC_RANGE_RE.search(summary_focus): + return True + if summary_categories and NUMERIC_SIGNAL_RE.search(summary_focus): + return True + return False + + +def _validate_counter_validation_summary(plan_folder: Path, profile: str) -> list[Finding]: + summary_path = plan_folder / "01-change-summary.md" + if not summary_path.is_file(): + return [] + + summary_text = summary_path.read_text(encoding="utf-8") + summary_focus = _summary_focus_text(summary_text) + if not summary_focus: + return [] + + coverage_text = _coverage_text(plan_folder, profile) + if not _has_observable_contract_requirements(coverage_text): + return [] + + if not any(pattern.search(summary_focus) for pattern in GENERIC_SUMMARY_PATTERNS): + return [] + + if _has_concrete_counter_validation_examples(summary_focus, coverage_text): + return [] + + examples = sorted(_identifier_signals(coverage_text))[:3] + example_suffix = f" Example signals: {', '.join(examples)}." if examples else "" + return [ + Finding( + "summary-missing-counter-validation-facts", + "Summary uses generic schema/output/data wording while the source-item coverage contains observable contract requirements. Keep the critical user-visible result criteria in Risultato atteso or Risorse coinvolte instead of only generic summary formulas." + + example_suffix, + "WARNING", + ) + ] + + def _normalize_route_cell(route_cell: str) -> str: normalized = route_cell.replace("`", " ") normalized = re.sub(r"\s+", " ", normalized) @@ -455,6 +576,8 @@ def _validate(plan_folder: Path, profile: str) -> list[Finding]: findings.extend(_validate_control_file(plan_folder)) findings.extend(_validate_extended_execution(plan_folder)) + findings.extend(_validate_counter_validation_summary(plan_folder, profile)) + return findings diff --git a/.github/skills/internal-gcp-governance/SKILL.md b/.github/skills/internal-gcp-governance/SKILL.md index ca9780a7..965c6a8c 100644 --- a/.github/skills/internal-gcp-governance/SKILL.md +++ b/.github/skills/internal-gcp-governance/SKILL.md @@ -5,6 +5,12 @@ description: Use when the user needs Google Cloud governance guidance for IAM op # Internal GCP Governance +## Referenced skills + +- `internal-gcp-strategic`: route back when direction or tradeoff framing is still unsettled. +- `internal-gcp-organization-structure`: route when org, folder, project, Shared VPC, or topology layout is the main decision. +- `internal-gcp-operations`: route when monitoring, reporting, backup, inventory, or post-rollout validation is the main need. + Use this skill when the next need is to define or review Google Cloud identity, access, and guardrail decisions. This skill owns governance logic after the broad structure is known. It helps separate org or folder guardrails from project-level grants and keeps permission decisions auditable. diff --git a/.github/skills/internal-gcp-operations/SKILL.md b/.github/skills/internal-gcp-operations/SKILL.md index 4347b443..4a30cf1d 100644 --- a/.github/skills/internal-gcp-operations/SKILL.md +++ b/.github/skills/internal-gcp-operations/SKILL.md @@ -5,6 +5,12 @@ description: Use when the user needs Google Cloud operational guidance for monit # Internal GCP Operations +## Referenced skills + +- `internal-gcp-strategic`: route back when direction or tradeoff framing is still unsettled. +- `internal-gcp-organization-structure`: route when org, folder, project, Shared VPC, or topology layout is the main decision. +- `internal-gcp-governance`: route when IAM, workload identity, service account, or Org Policy design is the main decision. + Use this skill when the next need is to validate, observe, or operationalize a GCP platform decision. This skill owns the operational side of the platform: monitoring, evidence, inventory, preflight, and post-rollout verification. It does not replace strategic framing, structure design, or governance design. diff --git a/.github/skills/internal-gcp-organization-structure/SKILL.md b/.github/skills/internal-gcp-organization-structure/SKILL.md index 09da8384..1d89a0c7 100644 --- a/.github/skills/internal-gcp-organization-structure/SKILL.md +++ b/.github/skills/internal-gcp-organization-structure/SKILL.md @@ -5,6 +5,12 @@ description: Use when the user needs Google Cloud control-plane or platform-stru # Internal GCP Organization Structure +## Referenced skills + +- `internal-gcp-strategic`: route back when direction or tradeoff framing is still unsettled. +- `internal-gcp-governance`: route IAM, service account, workload identity, or Org Policy design. +- `internal-gcp-operations`: route monitoring, backup, reporting, inventory, or post-rollout validation. + Use this skill when the next need is to design or review how GCP is structured at org and platform level. This skill owns GCP layout decisions, not generic strategy and not detailed IAM or monitoring implementation. It helps translate a platform goal into org, folder, project, Shared VPC, topology, and rollout structure. diff --git a/.github/skills/internal-gcp-strategic/SKILL.md b/.github/skills/internal-gcp-strategic/SKILL.md index 5a60b2a7..0131477f 100644 --- a/.github/skills/internal-gcp-strategic/SKILL.md +++ b/.github/skills/internal-gcp-strategic/SKILL.md @@ -5,6 +5,15 @@ description: Use when the user needs high-level Google Cloud platform decision s # Internal GCP Strategic +## Referenced skills + +- `internal-gcp-organization-structure`: route when org, folder, project, Shared VPC, or topology layout becomes the decision. +- `internal-gcp-governance`: route when IAM, workload identity, service account, or Org Policy design becomes the decision. +- `internal-gcp-operations`: route when monitoring, backup, reporting, inventory, or post-rollout validation becomes the decision. +- `internal-terraform`: route implementation work in Terraform or OpenTofu. +- `internal-python-script`: route implementation work in standalone Python automation. +- `internal-bash-script`: route implementation work in standalone Bash automation. + Use this skill when the main need is to reason about a GCP decision before implementation. This is a strategic support skill. It helps frame the decision, compare realistic options, expose tradeoffs, and recommend a direction. It does not implement the change and it does not choose Terraform, Python, or Bash on behalf of the user. diff --git a/.github/skills/internal-github-governance/SKILL.md b/.github/skills/internal-github-governance/SKILL.md index d3749f43..f91e6fe4 100644 --- a/.github/skills/internal-github-governance/SKILL.md +++ b/.github/skills/internal-github-governance/SKILL.md @@ -5,6 +5,11 @@ description: Use when the user needs GitHub governance guidance for rulesets, br # Internal GitHub Governance +## Referenced skills + +- `internal-github-strategic`: route back when enterprise, org, repo, or operating-model direction is still unsettled. +- `internal-github-operations`: route runner health, reporting, audit evidence, drift checks, or post-rollout validation. + Use this skill when the next need is to define or review GitHub permissions, guardrails, and policy decisions. This skill owns governance logic after the broad operating model is known. It helps separate enterprise or org guardrails from repo or environment grants and keeps permission decisions auditable. diff --git a/.github/skills/internal-github-operations/SKILL.md b/.github/skills/internal-github-operations/SKILL.md index 3407e38e..a84b90b9 100644 --- a/.github/skills/internal-github-operations/SKILL.md +++ b/.github/skills/internal-github-operations/SKILL.md @@ -5,6 +5,11 @@ description: Use when the user needs GitHub operational guidance for Actions hea # Internal GitHub Operations +## Referenced skills + +- `internal-github-strategic`: route back when enterprise, org, repo, or operating-model direction is still unsettled. +- `internal-github-governance`: route rulesets, permissions, Actions permissions, OIDC, secrets, environments, or guardrail design. + Use this skill when the next need is to validate, observe, or operationalize a GitHub platform decision. This skill owns the operational side of the platform: workflow health, runner evidence, preflight, and post-rollout verification. It does not replace strategic framing or governance design. diff --git a/.github/skills/internal-github-strategic/SKILL.md b/.github/skills/internal-github-strategic/SKILL.md index 84421fd0..d820fdc8 100644 --- a/.github/skills/internal-github-strategic/SKILL.md +++ b/.github/skills/internal-github-strategic/SKILL.md @@ -5,6 +5,14 @@ description: Use when the user needs high-level GitHub platform or operating-mod # Internal GitHub Strategic +## Referenced skills + +- `internal-github-governance`: route when rulesets, permissions, OIDC, secrets, environments, or guardrails become the decision. +- `internal-github-operations`: route when Actions health, runner operations, audit logs, drift, or evidence becomes the decision. +- `internal-github-actions`: route GitHub Actions workflow implementation. +- `internal-python-script`: route implementation work in standalone Python automation. +- `internal-bash-script`: route implementation work in standalone Bash automation. + Use this skill when the main need is to reason about a GitHub platform decision before implementation. This is a strategic support skill. It helps frame the decision, compare realistic options, expose tradeoffs, and recommend a direction. It does not implement the change and it does not choose Terraform, Python, or Bash on behalf of the user. diff --git a/.github/skills/internal-high-level-review/SKILL.md b/.github/skills/internal-high-level-review/SKILL.md index 9d10f79a..eed4ccda 100644 --- a/.github/skills/internal-high-level-review/SKILL.md +++ b/.github/skills/internal-high-level-review/SKILL.md @@ -14,7 +14,6 @@ or final claim. - `internal-code-review`: line-level defect review owner. - `internal-gateway-critical-master`: pressure-test owner when the main need is challenge rather than review evidence. -- `internal-security-review`: unavailable future security lens, used only after promotion creates the skill. - `superpowers-verification-before-completion`: evidence gate before claiming no systems findings or merge readiness. Use this skill as the systems-level owner for repository changes and unfamiliar @@ -35,7 +34,8 @@ operational context. - Use `internal-code-review` 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. -- Use a promoted `internal-security-review` only after that skill exists; until then, route security-specific gaps through the closest existing owner and state the gap. +- Route security-specific gaps through the closest existing owner and state the + missing specialized-owner gap when no promoted security owner exists. - Do not turn advisory architecture notes into mandatory changes without evidence. - Do not introduce `CONTEXT.md`, ADR folders, or glossary maintenance as a side effect of review unless those structures already exist and the user asks to @@ -47,7 +47,8 @@ operational context. - 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. -- Future `internal-security-review`: security, AI safety, trust boundaries, data, and secret exposure after promotion gates pass. +- Security-specific review depth stays with the closest existing owner until a + specialized security owner is promoted. ## Analysis dimensions @@ -201,6 +202,6 @@ Before presenting findings, verify: - Architecture recommendations must include impact and effort assessment. - Orientation maps must name the target area, domain vocabulary, module map, caller or entrypoint evidence, boundary notes, and uncertainty. -- Security-specific gaps must not imply `internal-security-review` exists before promotion. +- Security-specific gaps must not imply that an unpromoted security owner exists. - Use `superpowers-verification-before-completion` before claiming there are no systems findings, the review is complete, or the change is merge-ready. diff --git a/.github/skills/internal-high-level-review/references/audit-dispatch.md b/.github/skills/internal-high-level-review/references/audit-dispatch.md index 98eba101..ac877fed 100644 --- a/.github/skills/internal-high-level-review/references/audit-dispatch.md +++ b/.github/skills/internal-high-level-review/references/audit-dispatch.md @@ -54,7 +54,7 @@ The subagent returns only a report: - The subagent must not edit canonical files. - The subagent must not create or promote repository owners. -- The subagent must not decide that `internal-security-review` exists. +- The subagent must not decide that an unpromoted security owner exists. - The main assistant must spot-check at least one high or critical finding before using the report. - If all subagent output is speculative, treat it as evidence gaps rather than diff --git a/.github/skills/internal-high-level-review/references/review-lenses.md b/.github/skills/internal-high-level-review/references/review-lenses.md index 582bc6c8..5ce2edeb 100644 --- a/.github/skills/internal-high-level-review/references/review-lenses.md +++ b/.github/skills/internal-high-level-review/references/review-lenses.md @@ -75,6 +75,6 @@ Causal layer: Route: ``` -Do not use `internal-security-review` as an active owner until promotion creates -that skill. Route security-specific gaps through the closest existing owner and -state the promotion gap. +Do not use an unpromoted security review owner as active. Route +security-specific gaps through the closest existing owner and state the promotion +gap. diff --git a/.github/skills/internal-high-level-review/references/scope-drift.md b/.github/skills/internal-high-level-review/references/scope-drift.md index bc4b1c34..8bce0272 100644 --- a/.github/skills/internal-high-level-review/references/scope-drift.md +++ b/.github/skills/internal-high-level-review/references/scope-drift.md @@ -53,4 +53,4 @@ review, merge-readiness answer, or retained-plan completion report. - Do not hide reduced scope inside a success summary. Name the dropped item and why it was dropped. - Security-specific drift must be routed through the closest existing owner - until a promoted `internal-security-review` exists. + until a specialized security owner is promoted. diff --git a/.github/skills/internal-java-project/SKILL.md b/.github/skills/internal-java-project/SKILL.md index a83775fc..9ccb330c 100644 --- a/.github/skills/internal-java-project/SKILL.md +++ b/.github/skills/internal-java-project/SKILL.md @@ -5,6 +5,14 @@ description: Use when creating or modifying Java project code and the main conce # Java Project Skill +## Referenced skills + +Treat the referenced skill below as an on-demand owner. Do not preload it for +every Java project edit; load it only when Spring Boot framework behavior is +the main constraint. + +- `internal-java-spring-boot-development`: Spring Boot controllers, configuration, repositories, scheduling, and Spring test-slice depth. + ## When to use - Services, handlers, controllers, utilities, modules. @@ -12,7 +20,7 @@ description: Use when creating or modifying Java project code and the main conce ## When not to use -- Spring Boot framework behavior drives the work; use `internal-spring-boot-development`. +- Spring Boot framework behavior drives the work; use `internal-java-spring-boot-development`. - Build-system behavior is generic Make, YAML, or CI rather than Java-specific. ## Compact Java baseline @@ -23,12 +31,22 @@ description: Use when creating or modifying Java project code and the main conce - Use JUnit 5 for unit tests unless the repository has another established test stack. - Keep dependency, plugin, runtime, and test intent explicit in Maven or Gradle files. +## Boundary + +- Keep machine-readable output stable and undecorated at data boundaries, and keep human-friendly formatting at CLI or UI boundaries only. +- Keep logs structured with contextual keys and avoid mixing log streams with program output consumed by tools. +- Validate external input at transport and persistence boundaries before state changes. +- Use `Optional` at boundaries where absence is expected and avoid `null` as hidden control flow. + ## Project-specific guidance - Prefer constructor injection and immutable dependencies in Spring components. - Keep controllers thin, services stateless, and API DTOs separate from persistence entities. - Split-or-justify any class or service that trends toward a god class role with mixed responsibilities. - Use Java 21 features only when the project already targets them or the runtime requirement is explicit. +- Prefer immutable domain types and final fields by default. +- Prefer static factory methods when constructor intent is ambiguous. +- Prefer composition over inheritance unless bounded polymorphism is a true domain constraint. Load `references/examples.md` when you need a minimal class or test example. @@ -38,7 +56,10 @@ Load `references/examples.md` when you need a minimal class or test example. - Use `@ParameterizedTest`, `assertAll`, `@Nested`, and `@Tag` when they improve test clarity rather than just adding ceremony. - Use Spring test slices such as `@WebMvcTest` or `@DataJpaTest` before defaulting to full-context tests. - Use Testcontainers when integration tests need real databases or external dependencies. -- For modify tasks: edit implementation first, run existing tests, then update tests only for intentional behavior changes. +- For behavior changes or bug fixes, write or update the failing test first, then implement and re-run. +- For pure refactors, keep behavior stable and run compile plus existing tests before and after. +- Mock only external boundaries and keep internal collaborators real where practical. +- Prefer targeted coverage for changed behavior and boundary failures over broad percentage goals. ## Spring coordination diff --git a/.github/skills/internal-java-spring-boot-development/SKILL.md b/.github/skills/internal-java-spring-boot-development/SKILL.md index c89c9497..3e65fb04 100644 --- a/.github/skills/internal-java-spring-boot-development/SKILL.md +++ b/.github/skills/internal-java-spring-boot-development/SKILL.md @@ -36,7 +36,7 @@ affect the design, wiring, testing, or configuration. - Keep controllers thin: validate input, map transport types, delegate once, and return response DTOs. - Keep services stateless and focused on business behavior; avoid mutable singleton state. - Bind structured settings with `@ConfigurationProperties` instead of scattering `@Value` keys. -- Validate request DTOs with Bean Validation and `@Valid`. +- Validate transport DTOs and persistence-boundary commands with Bean Validation and `@Valid` before service execution. - Centralize API error mapping with `@ControllerAdvice` or the project's existing equivalent. - Keep transaction boundaries in the service layer and scope them only around the behavior that needs atomicity. - Do not leak JPA entities directly through HTTP contracts unless the project already made that tradeoff intentionally. diff --git a/.github/skills/internal-java/SKILL.md b/.github/skills/internal-java/SKILL.md index 58eef9f6..34876041 100644 --- a/.github/skills/internal-java/SKILL.md +++ b/.github/skills/internal-java/SKILL.md @@ -35,6 +35,11 @@ every Java edit; load them only when the task proves which owner is needed. - Add concise JavaDoc only when a new or changed core type has non-obvious intent. - Use JUnit 5 for unit tests unless the repository has another established test stack. - Keep dependency, plugin, runtime, and test intent explicit in Maven or Gradle files. +- Keep dependency delivery reproducible: no vendored jars or shaded copies, and pin versions with explicit properties or a BOM. +- Keep comments, JavaDoc, logs, exceptions, and operator-facing output in English. +- Centralize runtime configuration and keep domain invariants in code rather than environment toggles. +- Avoid mutable static state in application code. +- Validate boundary input fail-fast and reject invalid state early. ## Validation diff --git a/.github/skills/internal-lesson-codification/SKILL.md b/.github/skills/internal-lesson-codification/SKILL.md index 66c2a197..387772a2 100644 --- a/.github/skills/internal-lesson-codification/SKILL.md +++ b/.github/skills/internal-lesson-codification/SKILL.md @@ -5,6 +5,10 @@ description: Use before adding, updating, or codifying retained lessons, LESSONS # Internal Lesson Codification +## Referenced skills + +- None. + Use this skill to decide how a newly discovered lesson should be handled before editing `LESSONS_LEARNED.md` or any always-on guidance. This skill owns the codification workflow. It does not own the ledger row format, which stays in `LESSONS_LEARNED.md`. diff --git a/.github/skills/internal-nodejs-project/SKILL.md b/.github/skills/internal-nodejs-project/SKILL.md index 9ac0c3a8..21410cf4 100644 --- a/.github/skills/internal-nodejs-project/SKILL.md +++ b/.github/skills/internal-nodejs-project/SKILL.md @@ -5,6 +5,14 @@ description: Use when creating or modifying Node.js or TypeScript project code s # Node.js Project Skill +## Referenced skills + +Treat the referenced skill below as an on-demand owner. Do not preload it for +every project edit; load it when baseline Node.js or TypeScript metadata rules +are the primary concern. + +- `internal-nodejs`: baseline Node.js and TypeScript guidance for package metadata, lockfiles, scripts, and compiler settings. + ## When to use - Services, handlers, adapters, and utility modules. @@ -24,12 +32,22 @@ description: Use when creating or modifying Node.js or TypeScript project code s - Prefer strict `tsconfig.json` settings unless a documented compatibility reason exists. - Preserve the existing module system and package conventions unless the task explicitly changes them. +## Boundary + +- Keep machine-readable payloads stable and undecorated at data boundaries, and keep human-friendly formatting at CLI or UI boundaries only. +- Keep logs structured and do not mix log streams with stdout payloads consumed by other tools. +- Classify operational errors at boundaries and handle them centrally; let programmer errors fail fast. +- Validate external input with schema checks at API and module boundaries before domain logic runs. + ## Project-specific guidance - Follow the existing module system and runtime constraints before introducing ESM/CJS or build-tool changes. - Validate inputs at API or function boundaries and keep async error handling explicit. - Keep framework wiring thin and move request-shaping logic out of transport handlers when reuse or testing would improve. - Keep async boundaries explicit between transport handlers, domain modules, and infrastructure adapters. +- Use a central async error handler path instead of ad-hoc per-handler response logic. +- Keep the event loop non-blocking; move CPU-heavy work to worker threads, queues, or external services. +- Centralize environment-aware config loading and keep domain invariants out of env parsing. Load `references/examples.md` when you need a minimal module or test example. @@ -37,7 +55,11 @@ Load `references/examples.md` when you need a minimal module or test example. - Follow the repository test-stack defaults. - If the repository already uses Jest, stay with local Jest conventions instead of introducing mixed test stacks. -- For modify tasks: edit implementation first, run existing tests, then update tests only for intentional behavior changes. +- For behavior changes or bug fixes, write or update the failing test first, then implement and re-run. +- For pure refactors, run existing tests before and after while keeping public behavior unchanged. +- Prefer parameterized table-style tests when many input/output cases exercise one behavior. +- Mock only external boundaries and keep internal modules real where practical. +- Focus coverage on changed branches and boundary failure paths. ## Runtime and async guidance diff --git a/.github/skills/internal-nodejs/SKILL.md b/.github/skills/internal-nodejs/SKILL.md index d1883860..afa0272b 100644 --- a/.github/skills/internal-nodejs/SKILL.md +++ b/.github/skills/internal-nodejs/SKILL.md @@ -34,7 +34,10 @@ package architecture, or deterministic test design becomes the real issue. - Apply pragmatic DRY: extract repeated decision paths and shared adapters, but avoid speculative abstractions. - Use `node:test` and `node:assert/strict` unless the repository already standardizes on another test framework. - Keep `package.json` scripts, engines, and dependency intent explicit. -- Prefer strict `tsconfig.json` settings unless a documented compatibility reason exists. +- Keep dependency delivery reproducible: no vendored packages, commit the lockfile, and use `npm ci` for deterministic installs. +- Keep comments, JSDoc, logs, thrown errors, and operator-facing output in English. +- Centralize runtime configuration and keep domain invariants in code rather than environment toggles. +- Keep strict `tsconfig.json` settings enabled, avoid drift toward `any`, and type external boundaries explicitly. - Preserve the existing module system and package conventions unless the task explicitly changes them. ## Validation diff --git a/.github/skills/internal-oop-design-patterns/SKILL.md b/.github/skills/internal-oop-design-patterns/SKILL.md index 889da248..52f890cb 100644 --- a/.github/skills/internal-oop-design-patterns/SKILL.md +++ b/.github/skills/internal-oop-design-patterns/SKILL.md @@ -5,6 +5,10 @@ description: Use when refactoring object collaborations, replacing branching wit # Internal OOP Design Patterns +## Referenced skills + +- None. + Use this skill as a decision and refactoring workflow, not as a mandate to add pattern ceremony. Start from the actual change pressure in the code and prefer the smallest design that removes the current problem. ## When to use diff --git a/.github/skills/internal-python-project/references/common-mistakes.md b/.github/skills/internal-python-project/references/common-mistakes.md index a4c1eb43..3b2f8ada 100644 --- a/.github/skills/internal-python-project/references/common-mistakes.md +++ b/.github/skills/internal-python-project/references/common-mistakes.md @@ -6,10 +6,21 @@ | Mutable default arguments (`def f(items=[])`) | Shared state between calls — classic Python gotcha | Use `None` default + create inside function | | Bare `except:` or `except Exception:` | Swallows `KeyboardInterrupt`, `SystemExit` | Catch specific exceptions | | No type hints on public API | Hard to understand contracts, no static analysis | Add type hints on function signatures | +| Injecting every collaborator as a `*_fn` hook or alias shim | Hides real seams and makes call flow harder to follow | Inject only true external boundaries or variability points; call stable helpers directly | +| Copying shared helper logic across modules | Fixes drift and behavior diverges across call sites | Define the helper once in the owning module and import it | | Updating dependency requirements without refreshed hashes | Reproducible installs break or drift silently | Regenerate exact pins and hashes, then validate with `pip install --require-hashes -r requirements.txt` | | Tests that depend on execution order | Fragile test suite, non-deterministic failures | Each test must be self-contained | | Forcing async into CPU-bound or simple flows | Adds complexity without throughput benefit | Keep it synchronous unless I/O concurrency is the real bottleneck | +| HTTP client pools smaller than worker or task concurrency | Work queues behind the pool and hides throughput bottlenecks | Size connection pools and limits to match max worker or async concurrency | +| Claiming pending futures will fast-fail work that is already running | Misstates `concurrent.futures` cancellation semantics and hides partial execution | State cancellation limits honestly and handle already-running work explicitly | | Mocking internal implementation details | Makes tests brittle and hides real regressions | Mock only true external boundaries | | Using `rich`, emoji, tables, or panels outside human-facing CLI adapter reporting | Mixes terminal UI with project behavior or machine-readable output such as JSON | Keep project logs neutral or structured, keep data output plain, and put `rich` reporting in a CLI adapter | +| Silently trusting fuzzy or ambiguous parsing | Derived values look authoritative without provenance | Mark derived values explicitly and surface provenance or diagnostics | +| Repeating coupled labels, order, or rank literals in multiple places | Values drift and sorting bugs appear | Keep coupled value and ordering in one `Enum` or mapping source of truth | +| Building indexes or joins that silently overwrite duplicate keys | Collisions hide data loss and make debugging hard | Detect duplicate keys and surface a diagnostic or explicit merge policy | +| Mutating upstream payloads to smuggle context | Hidden side effects blur ownership and break reuse | Return wrappers, dataclasses, DTOs, or explicit context alongside the payload | +| Embedding full raw payloads in shareable outputs | Bloats artifacts and can leak sensitive data | Gate raw dumps behind an explicit flag or trace file; keep default reports summarized | | Treating line coverage as the goal | Inflates test volume without improving defect detection | Target coverage around changed behavior and risky paths | | God classes with 10+ methods | Hard to test, hard to reason about | Split by responsibility into focused classes | +| God functions that mix parsing, validation, orchestration, and side effects | Hidden branching and state make reuse and testing harder | Split the function into focused helpers with one responsibility each | +| Adding low-value re-export or alias shim modules | Hides the real owner and adds import indirection | Import the real owner directly unless a compatibility boundary is documented | diff --git a/.github/skills/internal-python-script/SKILL.md b/.github/skills/internal-python-script/SKILL.md index 509f942c..8a08cc71 100644 --- a/.github/skills/internal-python-script/SKILL.md +++ b/.github/skills/internal-python-script/SKILL.md @@ -31,7 +31,7 @@ description: Use when creating or modifying standalone Python scripts, CLIs, or ## Script-specific guidance - Standalone tools should default to a dedicated folder or toolkit root, not a loose top-level `.py` file. -- Keep entrypoints thin: parse arguments, resolve paths, orchestrate helpers, and return an exit code through `main() -> int` plus `raise SystemExit(main())`. +- Keep entrypoints thin and importable: prefer a package `__main__.py`, importable `cli.py`, or declared `console_scripts` entrypoint; parse arguments, resolve paths, orchestrate helpers, and return an exit code through `main() -> int` plus `raise SystemExit(main())`. - Keep script-owned configuration visible at the entrypoint boundary. In single-file scripts, place a clearly named `Configuration` section near the end of the file, after helper definitions and before `main()` or `raise SystemExit(main())`. - Name configuration values by purpose, not by type: paths, file names, field lists, thresholds, defaults, mappings, filters, and output modes should explain what behavior they control. - Do not hide script-specific configuration inside helper modules or libraries. Helpers should accept explicit parameters or a small typed settings object when several values travel together. @@ -51,6 +51,7 @@ description: Use when creating or modifying standalone Python scripts, CLIs, or - Add machine-readable output such as `--format json` only when the tool has a real automation consumer. Keep text output as the default operator path, and do not decorate machine-readable output with `rich`, emoji, color, or tables. - When machine-readable output can become large and the script is agent-facing, add a bounded mode such as `--format compact` that preserves status, blocker or finding counts, key path evidence, and next action without dumping full detail. - Keep full `--format json` available for durable audit/debug use; do not replace it with compact mode. +- When a script surfaces reusable project payloads, keep default reports summarized and defer raw payload dump policy to the project reporting boundary instead of embedding full bodies in shareable output. ## Compact Python baseline @@ -101,6 +102,7 @@ Keep these rules visible while drafting: - For refactors, prose-only updates, generated fixtures, or mechanical formatting with no executable behavior change, run the existing focused tests plus `py_compile` or `compileall` instead of manufacturing speculative tests. - When Ruff is configured, run `ruff format` for formatting-only Python edits and `ruff check` for lint feedback before wider test runs. - Prefer existing repository commands such as `make lint`, `make test`, or a shared script runner before inventing a one-off validation path. +- Keep test execution reproducible: run through the declared interpreter or local virtualenv, reuse shared runners when they exist, and anchor pytest discovery with the repository rootdir or `testpaths` contract instead of ad-hoc shell state. ## Runtime guidance diff --git a/.github/skills/internal-python-script/references/common-mistakes.md b/.github/skills/internal-python-script/references/common-mistakes.md index e104cfdb..b9a2080a 100644 --- a/.github/skills/internal-python-script/references/common-mistakes.md +++ b/.github/skills/internal-python-script/references/common-mistakes.md @@ -3,6 +3,7 @@ | Mistake | Why it matters | Instead | | --- | --- | --- | | Missing `if __name__ == "__main__":` guard | Script runs on import, breaks testing and reuse | Always guard the entry point | +| Using a hyphenated or file-only entrypoint that cannot be imported cleanly | Breaks `python -m`, packaging, and test reuse | Expose an importable `cli.py`, package `__main__.py`, or `console_scripts` entrypoint | | Using `print()` for errors | Errors go to stdout, mixed with normal output | Use `print(..., file=sys.stderr)` or `logging` | | Bare `except:` or `except Exception:` at top level | Swallows all errors including KeyboardInterrupt | Catch specific exceptions; let unexpected ones propagate | | Hardcoded file paths | Non-portable across machines | Use `argparse`, `pathlib`, or environment variables | diff --git a/.github/skills/internal-python/SKILL.md b/.github/skills/internal-python/SKILL.md index 77711c8a..eb08078d 100644 --- a/.github/skills/internal-python/SKILL.md +++ b/.github/skills/internal-python/SKILL.md @@ -39,6 +39,10 @@ every Python edit; load them only when the task proves script or project depth. - Do not confuse domain invariants with configuration. Stable rules that belong to the domain may stay near the domain code; environment-specific or operator-tuned values belong at an entrypoint, settings module, adapter, or composition boundary. - Keep comments, docstrings, logs, exceptions, and CLI output in English. - Use the repository-declared runtime before falling back to ambient `python3`. +- If a local `.venv` or declared runtime exists, use it first for + `py_compile`, `pytest`, and validator runs instead of ambient Python. +- Before opening large modules, use `rg` to find owner functions, classes, + entrypoints, tests, and runtime selectors, then read only the needed blocks. - When Ruff is configured for the target repository, let `ruff format` own formatting and use Ruff diagnostics for import order and simple style cleanup. Do not create manual formatting churn that fights the configured formatter. - When a test must modify `sys.path` before importing a standalone script, keep the affected import after that setup and mark only that import with `# noqa: E402`; remove truly unused imports or variables instead of suppressing them. - Add or update tests for testable logic. @@ -55,5 +59,8 @@ Use `internal-python-project` when importable behavior, service boundaries, appl ## Validation - Run the nearest focused `pytest` command when behavior changes. -- Run `python -m py_compile ` or `python -m compileall ` for syntax-only changes. +- Run the nearest runtime-owned `py_compile` or `compileall` command for + syntax-only changes. +- Keep compile and test scope narrow. Exclude `.venv`, `__pycache__`, exports, + generated outputs, and dependency trees from broad sweeps. - Use the repository wrapper or runtime selector when one exists. diff --git a/.github/skills/internal-skill-creator/SKILL.md b/.github/skills/internal-skill-creator/SKILL.md index 47dafb7f..289013dc 100644 --- a/.github/skills/internal-skill-creator/SKILL.md +++ b/.github/skills/internal-skill-creator/SKILL.md @@ -10,7 +10,7 @@ description: Use first when creating, splitting, replacing, or materially revisi This index lists every other skill that this file asks the agent to load, route to, compare against, or delegate to. Keep it current before changing downstream wording. Self-references to `internal-skill-creator` identify this current skill and are not a dependency. - `openai-skill-creator`: generic bundle anatomy, reusable-resource layout, `agents/openai.yaml`, initialization workflow, and structural validation after the local boundary is clear. -- `local-agent-sync-external-resources`: catalog-governance operating engine behind `local-sync-external-resources` when work becomes sync-managed catalog maintenance, external refresh, or inventory-wide keep/update/extract/retire decisions. +- `local-agent-sync-external-resources`: catalog-governance operating engine behind the local sync command center when work becomes sync-managed catalog maintenance, external refresh, or inventory-wide keep/update/extract/retire decisions. - `internal-agent-creator`: repository-owned agent authoring and agent/skill boundary rewrites. Use this skill as the canonical repository-owned first entrypoint for skill authoring in this repository. diff --git a/.github/skills/local-agent-sync-external-resources/SKILL.md b/.github/skills/local-agent-sync-external-resources/SKILL.md index 5c54bb57..08b4d523 100644 --- a/.github/skills/local-agent-sync-external-resources/SKILL.md +++ b/.github/skills/local-agent-sync-external-resources/SKILL.md @@ -5,6 +5,13 @@ description: Use when maintaining the sync-managed `.github/` catalog behind `lo # Internal Agent Sync External Resources +## Referenced skills + +- `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`. 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. 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 13c08f9c..6de1def0 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 @@ -5,6 +5,10 @@ description: Use when aligning a consumer repository to this repository's manage # Internal Agent Sync Global Copilot Configs Into Repo +## Referenced skills + +- `mattpocock-caveman`: optional compression support after target-drift or sync gates are clear. + 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. @@ -16,7 +20,7 @@ The paired agent should not restate default mode handling, preserved `local-*` b ## When to use - 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 bridge model after mirroring. +- 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 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/sync_copilot_catalog.sh` or `.github/scripts/sync_copilot_catalog.py`. @@ -32,11 +36,11 @@ The paired agent should not restate default mode handling, preserved `local-*` b - Exclude source resources named `internal-sync-*` from consumer mirroring and remove any target copies of those resources during `apply`. - Create consumer-local `docs/README.md`, `docs/repository-context.md`, `docs/architecture.md`, `docs/tech.md`, and `docs/structure.md` from `.github/templates/` only when missing, then preserve target-authored content on later sync runs. - Delete retired standalone runtime operating model documents from consumers; runtime workflow guidance now travels through root guidance and skills. -- Keep root guidance layered: `AGENTS.md` is the bridge, `.github/copilot-instructions.md` is the compact Copilot routing bridge, `.github/instructions/copilot-code-review.instructions.md` owns global review behavior, and `.github/INVENTORY.md` is the live catalog. +- Keep root guidance layered: `AGENTS.md` is the agent policy entrypoint, `.github/copilot-instructions.md` is review-only for GitHub.com Copilot code review, and `.github/INVENTORY.md` is the live catalog. - Treat `LESSONS_LEARNED.md` as a source-managed retained-learning template: create it when missing, keep its structure aligned with the source contract, and preserve target-authored pending lessons instead of overwriting them with source rows. - Mirror only the explicitly shared repository-hygiene files declared in `references/sync-contract.md`; do not widen workflow or root-file mirroring implicitly. - Ensure the target repository `.gitignore` contains an ignore rule for `tmp/superpowers/`. -- Treat `.vscode/settings.json` as consumer-owned JSONC and manage only two Copilot keys field-by-field: `github.copilot.chat.codeGeneration.useInstructionFiles=false` and `chat.instructionsFilesLocations[".github/instructions"]=false`. +- Treat `.vscode/settings.json` as consumer-owned JSONC and manage only the Copilot settings required to disable instruction-file loading. - When moving from `plan` to `apply` against the same target, pass `--allow-dirty-target` only when the generated `tmp/copilot-sync.plan.md` is the sole target diff left by the planning run. - Prefer the bundled sync automation when it matches the requested mode instead of re-deriving the workflow manually. - Keep detailed operating rules in `references/sync-contract.md` instead of re-expanding them in the agent body. diff --git a/.github/skills/local-agent-sync-global-copilot-configs-into-repo/references/sync-contract.md b/.github/skills/local-agent-sync-global-copilot-configs-into-repo/references/sync-contract.md index 1f41d516..8b4e9c7f 100644 --- a/.github/skills/local-agent-sync-global-copilot-configs-into-repo/references/sync-contract.md +++ b/.github/skills/local-agent-sync-global-copilot-configs-into-repo/references/sync-contract.md @@ -11,7 +11,6 @@ Mirror or structurally align these source-managed paths into the consumer reposi - `.editorconfig` - `.pre-commit-config.yaml` - `.github/copilot-instructions.md` -- `.github/instructions/copilot-code-review.instructions.md` - `.github/copilot-commit-message-instructions.md` - `.github/security-baseline.md` - `.github/DEPRECATION.md` @@ -38,6 +37,7 @@ When a consumer-local creator depends on shared runtime-critical rules, keep a s - If a target has legacy `docs/01-local-architecture.md` or `docs/01-architecture.md` and lacks `docs/architecture.md`, rename the legacy file to the canonical path. If canonical and legacy paths coexist, block apply and require manual reconciliation. - If a target has legacy `docs/02-local-repository-context.md` or `docs/02-repository-context.md` and lacks `docs/repository-context.md`, rename the legacy file to the canonical path. If canonical and legacy paths coexist, block apply and require manual reconciliation. - Delete legacy `docs/runtime-fit.md` and retired standalone runtime operating model documents. Runtime workflow guidance now lives in root guidance and skills. +- Do not mirror source `.github/instructions/**` files. Delete target non-`local-*` instruction files and preserve target-owned `local-*` instruction files as local exceptions. - Delete target-owned non-`local-*` assets inside mirrored categories during `apply`. - Keep the target target-agnostic. The default assumptions are only `.github/` and root `AGENTS.md`. - Ensure target root `LESSONS_LEARNED.md` exists. If it already exists, align it to the current source structure and migrate preserved pending lesson rows when the source table shape changes. @@ -49,18 +49,17 @@ When a consumer-local creator depends on shared runtime-critical rules, keep a s When root guidance is in scope, keep the target files in these roles: -- `AGENTS.md`: strategic bridge, precedence anchor, naming contract, and cross-surface routing guidance +- `AGENTS.md`: strategic entrypoint, precedence anchor, naming contract, and runtime agent policy - `LESSONS_LEARNED.md`: retained-learning ledger template aligned from source structure while preserving target-authored pending lessons; it remains non-canonical and repo-local in content - `docs/README.md`: consumer-local routing guide for knowledge documents scaffolded only when missing and then preserved - `docs/repository-context.md`: consumer-local descriptive context scaffolded only when missing and then preserved; it does not override policy - `docs/architecture.md`: consumer-local architecture contract scaffolded only when missing and then preserved - `docs/tech.md`: consumer-local technology contract scaffolded only when missing and then preserved - `docs/structure.md`: consumer-local structure contract scaffolded only when missing and then preserved -- `.github/copilot-instructions.md`: compact repo-wide GitHub Copilot routing bridge -- `.github/instructions/copilot-code-review.instructions.md`: source-managed global code review baseline +- `.github/copilot-instructions.md`: review-only GitHub.com Copilot code review behavior - `.github/INVENTORY.md`: exact live catalog generated from target filesystem state -Do not flatten these roles into one file. Do not let target `AGENTS.md` become an inventory dump or a second full copy of `.github/copilot-instructions.md`. +Do not flatten these roles into one file. Do not let target `AGENTS.md` become an inventory dump or a second full copy of review-only Copilot assets. ## Tracking Plan Lifecycle 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 ad947d42..c9e51272 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md +++ b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md @@ -10,9 +10,11 @@ description: Use when planning, auditing, or applying allowlisted home-directory - None. Use this skill as the operating engine for `.github/agents/local-sync-install-ai-resources.agent.md`. -Canonical command examples use `python3 ./.github/scripts/sync_home_ai_resources.py --format report`. +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. -The paired agent is a thin UX wrapper; this skill owns business logic, sequencing, approval posture, safety gates, and reporting for repo to home sync and bisync. Keep user-visible output deterministic and summary-first: status line, `Summary`, `Changes`, `Attention`, `Validation`, optional `Remaining Work`, and `Next`. Use tables only for change lists, blockers, and action details that benefit from columns. Keep detailed checklists in `references/` and deterministic rendering logic in `scripts/`. +Canonical command examples use `python3 ./.github/scripts/sync_home_ai_resources.py --format report`. ## When to use @@ -31,37 +33,42 @@ The paired agent is a thin UX wrapper; this skill owns business logic, sequencin ## Deterministic Operator Protocol -Every mode has exactly one command. Do not infer the mode, do not skip blockers, and do not treat `next_action` as user approval for `apply`. Plain `apply` and `bisync apply` still require an explicit user request. The `sync` command is the only auto-execute exception, and it may write only through the install lane after a zero-blocker, no-drift preflight. +Every mode has exactly one command. Do not infer the mode, do not skip blockers, and do not treat `next_action` as user approval for `apply`. Plain `apply` and `bisync apply` still require an explicit user request. The `sync` command is the only auto-execute exception: it may write only through the install lane after a zero-blocker, no-drift preflight. -### Lane Selection +### Command Map | User request | Lane | Command | | --- | --- | --- | -| Generic `sync` without a mode | Auto-run safe repo-to-home install for `skills`, then run `bisync plan` as a review gate | `python3 ./.github/scripts/sync_home_ai_resources.py sync --targets skills --home-root ~ --format report` | -| `bisync plan` | Bidirectional drift detection (read-only) | `python3 ./.github/scripts/sync_home_ai_resources.py bisync plan --home-root ~ --format report` | -| `bisync apply` | Bidirectional drift resolution (writes to both sides) | `python3 ./.github/scripts/sync_home_ai_resources.py bisync apply --home-root ~ --format report` | -| `plan` | Install lane dry run | `python3 ./.github/scripts/sync_home_ai_resources.py plan --targets --home-root ~ --format report` | -| `audit` | Compare source, manifest, and target paths | `python3 ./.github/scripts/sync_home_ai_resources.py audit --targets --home-root ~ --format report` | -| `doctor` | Readiness checks for runtime roots and support matrix | `python3 ./.github/scripts/sync_home_ai_resources.py doctor --targets --home-root ~ --format report` | -| `apply` | Install lane materialization | `python3 ./.github/scripts/sync_home_ai_resources.py apply --targets --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 | `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` | + +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`. -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` (defaults to `$HOME`). -When the desired active runtimes change, pair `--retire-targets ` with `--prune-managed` to remove runtime-specific managed copies and drop those targets from the manifest while keeping the remaining targets active. +### Mode Selection + +- `sync`: default safe automation for shared skills. Auto-apply clean repo-to-home install work, then stop only on home-owned or ambiguous bisync drift. +- `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. +- `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. ### Default Sync Sequence When the user says "sync" without a mode: 1. Run `sync` for the default `skills` target. -2. The command builds an install-lane `apply` plan and stops before writing when blockers, residual drift, missing directory creation, stale managed resources, destructive cleanup, or other manual gates are present. +2. The command builds an install-lane `apply` plan and stops before writing when blockers, missing directory creation, stale managed resources, destructive cleanup, or other manual gates are present. 3. If the install lane is clean, the command applies repo-to-home materialization and reports copied, skipped, validation, state, and manifest evidence. -4. After install, the command runs `bisync plan` as a review gate. Stop and ask for user direction when bisync reports any drift or blocker, including repo-to-home, home-to-repo, only-home, only-repo, or equal-mtime entries. -5. If bisync reports zero drift and zero blockers, report `done`. Do not run `bisync apply` automatically. +4. After install, the command runs `bisync plan` as a review gate. Stop and ask for user direction only when bisync reports `home-to-repo`, `only-home`, or `equal-mtime` drift, or another non-safe blocker. +5. `repo-to-home` and `only-repo` bisync entries are safe informational leftovers for the default lane. Report them, but do not stop the sync run for them. Do not run `bisync apply` automatically. Install must run before bisync because bisync modifies `~/.agents/skills/` directories that the install manifest tracks. Running install first copies fresh content from the repo with matching manifest hashes; bisync then finds both sides already aligned, avoiding spurious `target-modified-managed` blockers. -When the user explicitly asks for manual apply, run install `apply` first, then `bisync apply` only when the bisync plan has been reviewed and approved. Both plain apply lanes remain explicit. - ### Stop Conditions Stop and report when any of these occur: @@ -70,165 +77,85 @@ Stop and report when any of these occur: - `next_action.allowed` is `false`, except for `sync` reports that have already completed their safe install-lane work and are reporting `done`. - `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 any bisync drift or blocker after install. Treat this as a review state, not an apply failure. +- `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`. - 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`. -### Output - -Deterministic report output is available as `--format report` and should be the default for user-facing runs. Machine-readable output remains available as `--format compact` or `--format json` for automation and debugging. The payload always includes: - -- `next_step`: human-readable next instruction (backward compatible). -Canonical command examples use `python3 ./.github/scripts/sync_home_ai_resources.py --format report`. - -Report `next_action` to the user. Do not execute `command` from `next_action` unless the user explicitly asks. The only exception is the top-level `sync` command, which owns its own bounded install-lane apply decision and still stops before bisync apply. - ## Core Operating Contract - For the install lane, treat this repository as the source of truth for allowlisted home-sync resources. - Install sync is unidirectional: repo -> home only. Block any attempt to sync from home to repo. - Default generic sync requests to `sync`; keep plain `apply`, prune, directory creation, and all `bisync apply` writes explicit unless the user provided the matching flags or request. -- Limit v1 default materialization to documented direct-copy skill families and allowlisted agent translations for Codex and OpenCode. +- Limit v1 materialization to documented direct-copy skill families and allowlisted agent translations for Codex and OpenCode. - Preserve unmanaged target-local files and directories. -- Prune only stale managed assets, including manifest-managed resources whose source bundle was removed from the repo, and only when explicit approval is present and the manifest entry passes schema validation, path confinement, and content-hash drift checks. +- Prune stale managed assets only when explicit approval is present and the manifest entry passes schema validation, path confinement, and content-hash drift checks. - Keep local sync state under `~/.sync/cloud-strategy-governance/home-ai-resources/`. -- Block `apply` when runtime support is undocumented, target paths are unsafe, or ownership evidence is missing. -- Keep runtime support evidence explicit through the paired references instead of inferring undocumented home paths. +- Block writes when runtime support is undocumented, target paths are unsafe, ownership evidence is missing, the manifest is corrupt, or the source root sits under home sync state. - Use `--retire-targets` when the managed target set should shrink, for example removing `opencode` while keeping `codex` and `copilot`. +- Accept `codex`, `copilot`, `opencode`, comma-separated combinations, `cross`, `all`, or `tutto`; normalize and order targets deterministically. +- Keep `references/home-sync-catalog.yaml` as policy and explicit non-skill resources only; skill bundles are auto-discovered from `.github/skills/` when `include_unlisted_skills` is true. ## Bisync Lane The `bisync` lane provides explicit bidirectional synchronization between `.github/skills/` and `~/.agents/skills/`. It is a separate lane from install sync. -### Commands - -- `bisync plan`: detect drift (read-only). Reports `repo-to-home`, `home-to-repo`, `only-repo`, `only-home`, and `equal-mtime` entries, with explicit winner and blocker context. -- `bisync apply`: resolve drift by copying the winner bundle to the loser side. Applies `repo-to-home`, `home-to-repo`, and valid `only-repo` creation entries. Blocks on `only-home` and `equal-mtime`. - -### Safety Gates - - Blocks `apply` when the source repository has uncommitted or untracked changes. - Blocks `apply` when any `only-home` or `equal-mtime` entry exists. - Blocks `apply` when post-copy hash verification fails. - Blocks `apply` when post-apply plan still shows residual drift. -- Excludes `local-agent-sync-*` bundles and runtime artifacts (`.venv`, `__pycache__`, `.pytest_cache`, `.pyc`, `.pyo`) from scanning and copying. - -### Conflict Resolution - -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` (defaults to `$HOME`). - +- Excludes all `local-*` bundles and runtime artifacts (`.venv`, `__pycache__`, `.pytest_cache`, `.pyc`, `.pyo`) from scanning and copying. - `only-repo`: when not excluded, `bisync apply` can create the bundle in home from the repository side. - `only-home`: manual intervention required. Decide whether to keep it only in home, remove it, or add it to the repository. - `equal-mtime`: hashes differ but mtime is equal. Manual decision required because the winner cannot be determined from timestamps alone. ## Reporting Contract -Use a summary-first report for every mode. Do not dump raw JSON unless the user explicitly asks for it. Lead with one short status line that states lane, mode, result, blocker count, and relevant drift or target counts. +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. + +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`. Then follow the exact text layout in `references/sync-contract.md`: -- `doctor`: readiness summary plus a blocker table that answers what failed, why it matters, and what the user must do next. -- `sync`, `plan`, `audit`, and `bisync plan`: a compact summary, a change table when there are changes, and an attention table when there are blockers or drift decisions. For every proposed modification, explain the decision cause, for example repo copy is newer, home copy is newer, a managed resource is stale, or runtime support is not documented enough for apply. +- `doctor`: `Status`, `Summary`, `Readiness`, `Validation`, and `Next`. Show non-ok readiness checks and tell the user what blocks the next write. +- `sync`: `Status`, `Summary`, `Auto-applied`, `Stopped on`, `Validation`, and `Next`. Summarize counts first, then show only the copied resources and the exact drift or blockers that stopped completion. +- `plan`, `audit`, and `bisync plan`: a compact summary, a change table when there are changes, and an attention table when there are blockers or drift decisions. For every proposed modification, explain the decision cause, for example repo copy is newer, home copy is newer, a managed resource is stale, or runtime support is not documented enough for apply. - `apply` and `bisync apply`: a compact summary, an actions-performed table for writes, and a residual-issues table when needed. List copied, updated, pruned, or created resources and state why they were handled that way and how they were verified. Summarize unchanged managed resources by count instead of listing every skip. -- Include a human-friendly lane label in the status line for install and bisync reports, such as `repo-to-home install` and `repo-home drift`, so the mode is easier to read at a glance. - -Never report blocker codes alone. Translate each code into a plain-language reason and the required follow-up. Never say a resource will change without stating what evidence selected the winner or triggered the recommendation. When nothing changes, say so explicitly and still report validation and `next_action`. - -## Output Expectations - -- One-line status header with mode, selected targets, overall status, blocker count, and `next_action.action`. -- Selected mode, selected targets, and why that mode is valid. -- Source resources considered and the runtime support evidence used. -- A mode-appropriate summary-first layout from `references/sync-contract.md`: - - readiness and blocker table for `doctor` - - planned changes plus attention tables for `sync`, `plan`, `audit`, and `bisync plan` - - completed actions plus residual issues tables for `apply` and `bisync apply` -- Missing directories, conflicts, or documentation gates that block `apply`. -- For every blocked path, conflict, stale-managed entry, or non-ok doctor check, include a human-readable motivation that explains the policy or safety reason behind the recommendation. -- For every proposed or completed modification, include the reason the tool chose that action, such as newer repo content, newer home content, stale managed state, prune approval, or preserved unmanaged content. -- Repo/home bucket labels and winner/blocker summaries for bisync output. -- Managed versus preserved target-local outcomes and any approved prune behavior. -- Validation results, remaining blockers, and explicit validation gaps. -- `next_step` (text) and `next_action` (structured object). - -## Mode Selection - -- `sync`: default safe automation for shared skills. Auto-apply only clean install-lane repo-to-home work, then stop on any bisync drift or blocker. -- `plan`: produce a readable dry run and machine-readable state. -- `audit`: compare source, manifest, and managed target paths without writing runtime files. -- `doctor`: verify runtime roots, permissions, symlink posture, manifest health, and support-matrix readiness. -- `apply`: explicit only. Materialize only approved and safe operations. -- `dry-run`: alias of `plan`, not a separate behavior. -- `bisync plan`: bidirectional drift detection between `.github/skills/` and `~/.agents/skills/`. -- `bisync apply`: bidirectional drift resolution. Write only after preflight passes. - -## Target Selection - -- Accept `codex`, `copilot`, `opencode`, comma-separated combinations, `cross`, `all`, or `tutto`. -- Normalize whitespace, deduplicate, and order targets deterministically. -- Resolve skill roots as `~/.agents/skills` for all targets (scenario B: unification). -- When multiple targets resolve to the same physical path, perform the copy operation only once (physical deduplication). -- After apply, verify every copied resource by re-reading the target and comparing hashes. -- Block reverse sync: source root must not be under the home sync state directory. - -## Source And Materialization Policy - -- Read the runtime contract from `references/runtime-support-matrix.yaml` and the readable summary in `references/runtime-support-matrix.md`. -- Read the source allowlist from `references/home-sync-catalog.yaml`. -- Include only allowlisted `skills` and `agents` in v1. -- Copy managed resources instead of creating symlinks. -- Translate allowlisted `.agent.md` sources deterministically for Codex and OpenCode targets. -- Preserve target-local content that is outside the manifest. -- Record source hashes, expected content hashes, and managed target paths in the local manifest. -- Keep the manifest target set aligned with the requested active targets; retired targets leave the manifest only through an explicit run that names them in `--retire-targets`. -- Exclude runtime-generated bundle artifacts such as `.venv`, `__pycache__`, `.pytest_cache`, `.pyc`, and `.pyo` from hashes and copies. + +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. ## 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`. -- Keep library behavior inside `scripts/home_syncing.py`, bisync logic inside `scripts/bisync_skills.py`, and reference loading inside `scripts/home_sync_contract.py`. - -## Install Safety Gates - -- Block unmanaged overwrite. -- Block managed overwrite when the target content diverged from the last recorded manifest. -- Block stale managed delete when the manifest entry is invalid, escapes the expected runtime root, or the file content drifted from the recorded hash. -- Block unsafe home paths, unsupported symlink hops, missing target roots without explicit create approval, and undocumented runtime claims. -- Block `bisync apply` on dirty repository, `only-home`, `equal-mtime`, and post-apply verification failure. -- Keep the canonical error taxonomy in `references/error-codes.md`. -- Keep the doctor checklist in `references/doctor-checks.md`. - -## Install Conflict Resolution +- 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`. -When plan or audit reports blocked paths, resolve them before apply: +## Conflict Resolution -- `target-exists-unmanaged`: the target file or directory exists at home but is not in the sync manifest. Remove it manually so sync can recreate it from source. -- `target-modified-managed`: the target is in the manifest but its content diverged from the last recorded hash. Re-run install `plan` after a verified repo-to-home bisync; if the blocker remains, review the path as a genuine local divergence. -- `stale-managed` with a removed source bundle: the resource was managed previously but its source bundle no longer exists in the repo. Re-run with `--prune-managed` after review to remove the stale managed copy. -- `retire-target-overlap`: the same target was requested as both active and retired. Remove the overlap and re-run. -- After removing conflicting files, re-run plan to confirm zero blockers before applying. +When plan, audit, or sync reports blocked paths, resolve them before apply: -When `bisync plan` reports blocker entries, resolve them before `bisync apply`: +- `target-exists-unmanaged`: target content exists at home but is not manifest-managed. Review and move or remove it manually before rerunning plan. +- `target-modified-managed`: manifest-managed content diverged from the recorded hash. If home is clearly newer, let bisync surface the explicit home-to-repo decision; if it persists after verified bisync reconciliation, treat it as real local divergence. +- `stale-managed`: previously managed content is no longer planned. Re-run with `--prune-managed` only after review. +- `retire-target-overlap`: the same target was requested as active and retired. Remove the overlap and rerun. +- `bisync-only-home`: decide whether to keep it only in home, remove it, or add it to the repository. +- `bisync-equal-mtime`: choose the winning side and touch the winner to advance mtime. +- `bisync-repo-dirty`: commit or stash, or run `bisync apply` from a clean detached worktree with `--source-root`. -- `only-repo`: this is actionable during explicit `bisync apply` and creates the missing home bundle from the repository side when the bundle is valid and not excluded. -- `bisync-only-home`: the skill exists only in the home directory. Remove from home manually or decide to add to repo. -- `bisync-equal-mtime`: hashes differ but mtime is equal for both sides. Decide which side wins and touch the winner to advance mtime. -- `bisync-repo-dirty`: repository has uncommitted or untracked changes. Commit or stash, or run `bisync apply` from a clean detached worktree by setting `--source-root` to that clean checkout. +After any manual cleanup, re-run `plan` or `bisync plan` and require zero blockers before any explicit apply. ## Load On Demand -- Read `references/runtime-support-matrix.md` when the runtime family or support level decides the mode. -- Read `references/sync-contract.md` for state files, manifest fields, materialization rules, bisync contract, and reporting requirements. +- Read `references/runtime-support-matrix.yaml` when the runtime family or support level decides the mode. +- Read `references/sync-contract.md` for state files, manifest fields, materialization rules, doctor readiness, bisync contract, and reporting requirements. - Read `references/error-codes.md` when the correct blocking code or remediation must be surfaced. -- Read `references/doctor-checks.md` when readiness validation or local remediation steps matter. +- Read `references/home-sync-catalog.yaml` only when changing default discovery policy or explicit agent resources. ## Validation - Rebuild `.github/INVENTORY.md` when the bundle or related scripts change by using `./.github/scripts/build_inventory.sh --root .`. - Run `./.github/scripts/check_catalog_consistency.sh --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/sync_home_ai_resources.sh` after shell wrapper changes. -- Run focused agent or skill contract tests for the touched bundle. -- Run focused sync tests for target parsing, support-matrix policy, manifest handling, overwrite gates, bisync protocol, and missing-directory behavior when automation changes. +- 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 634b2134..01f917ad 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 plan, apply, audit, doctor, or bisync an allowlisted home-directory AI resource sync. Ask for an explicit mode if not provided." \ No newline at end of file + 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." diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/doctor-checks.md b/.github/skills/local-agent-sync-install-ai-resources/references/doctor-checks.md deleted file mode 100644 index c5b7099e..00000000 --- a/.github/skills/local-agent-sync-install-ai-resources/references/doctor-checks.md +++ /dev/null @@ -1,41 +0,0 @@ -# Doctor Checks - -Use this checklist when `doctor` needs to explain readiness before a local home sync. - -## Motivation rule - -For every check that is not `ok`, include a short **motivation** that explains why the current state triggers the reported status and why that status matters for the next mode the user may run. Do not report only the symptom; state the policy or safety reason behind the recommendation. - -## Common Checks - -- Confirm the selected runtime targets are known. -- Confirm the runtime support matrix is readable and current. -- Confirm the allowlist catalog exists and only references repository skill bundle paths. -- Confirm the state root under `~/.sync/cloud-strategy-governance/home-ai-resources/` is readable and writable. -- Confirm the manifest is either absent for a first run or parseable for later runs. -- Confirm every resolved target path stays under the expected home root. -- Confirm no disallowed symlink escape exists in the selected target tree. - -## Codex Checks - -- Confirm `~/.agents/skills` exists or can be created safely. -- Confirm `~/.codex/agents` exists or can be created safely. -- Confirm allowlisted skill bundles contain `SKILL.md`. -- Confirm allowlisted agent source files are valid `.agent.md` files. -- Confirm bundle-relative `references/`, `scripts/`, `assets/`, and `agents/openai.yaml` paths stay self-contained. - -## Copilot Checks - -- Confirm `~/.agents/skills` exists or can be created safely. -- Confirm `~/.copilot/agents` exists or can be created safely. -- Confirm allowlisted skill bundles contain `SKILL.md`. -- Confirm allowlisted agent source files are valid `.agent.md` files. -- Confirm bundle-relative paths stay self-contained. - -## OpenCode Checks - -- Confirm `~/.agents/skills` exists or can be created safely. -- Confirm `~/.config/opencode/agents` exists or can be created safely. -- Confirm allowlisted skill bundles contain `SKILL.md`. -- Confirm allowlisted agent source files are valid `.agent.md` files. -- Confirm bundle-relative paths stay self-contained. 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 7630ecec..463b1c49 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 @@ -2,959 +2,45 @@ version: 1 defaults: include_internal_skills: true include_local_skills: false - include_unlisted_skills: false + include_unlisted_skills: true + skill_targets: + - codex + - copilot + - opencode resources: -- resource_id: antigravity-api-design-principles - source_family: skills - source_path: .github/skills/antigravity-api-design-principles - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Portable reference skill with direct-copy bundle shape. -- resource_id: awesome-copilot-agentic-eval - source_family: skills - source_path: .github/skills/awesome-copilot-agentic-eval - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Portable evaluation workflow skill with repository-contained references. -- resource_id: openai-skill-creator - source_family: skills - source_path: .github/skills/openai-skill-creator - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Portable authoring skill that already ships deterministic helper scripts. -- resource_id: antigravity-aws-cost-optimizer - source_family: skills - source_path: .github/skills/antigravity-aws-cost-optimizer - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: antigravity-cloudformation-best-practices - source_family: skills - source_path: .github/skills/antigravity-cloudformation-best-practices - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: antigravity-golang-pro - source_family: skills - source_path: .github/skills/antigravity-golang-pro - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: antigravity-grafana-dashboards - source_family: skills - source_path: .github/skills/antigravity-grafana-dashboards - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: antigravity-kubernetes-architect - source_family: skills - source_path: .github/skills/antigravity-kubernetes-architect - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: antigravity-network-engineer - source_family: skills - source_path: .github/skills/antigravity-network-engineer - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: awesome-copilot-azure-devops-cli - source_family: skills - source_path: .github/skills/awesome-copilot-azure-devops-cli - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: awesome-copilot-azure-pricing - source_family: skills - source_path: .github/skills/awesome-copilot-azure-pricing - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: awesome-copilot-azure-resource-health-diagnose - source_family: skills - source_path: .github/skills/awesome-copilot-azure-resource-health-diagnose - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: awesome-copilot-azure-role-selector - source_family: skills - source_path: .github/skills/awesome-copilot-azure-role-selector - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: awesome-copilot-cloud-design-patterns - source_family: skills - source_path: .github/skills/awesome-copilot-cloud-design-patterns - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: awesome-copilot-codeql - source_family: skills - source_path: .github/skills/awesome-copilot-codeql - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: awesome-copilot-dependabot - source_family: skills - source_path: .github/skills/awesome-copilot-dependabot - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: awesome-copilot-secret-scanning - source_family: skills - source_path: .github/skills/awesome-copilot-secret-scanning - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: grill-me - source_family: skills - source_path: .github/skills/grill-me - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-ai-resource-review - source_family: skills - source_path: .github/skills/internal-ai-resource-review - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-agent-creator - source_family: skills - source_path: .github/skills/internal-agent-creator - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-agent-support-lane-change-engine - source_family: skills - source_path: .github/skills/internal-agent-support-lane-change-engine - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-agent-support-next-step - source_family: skills - source_path: .github/skills/internal-agent-support-next-step - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-aws-governance - source_family: skills - source_path: .github/skills/internal-aws-governance - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-aws-mcp-research - source_family: skills - source_path: .github/skills/internal-aws-mcp-research - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-aws-operations - source_family: skills - source_path: .github/skills/internal-aws-operations - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-aws-organization-structure - source_family: skills - source_path: .github/skills/internal-aws-organization-structure - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-aws-lambda - source_family: skills - source_path: .github/skills/internal-aws-lambda - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-aws-strategic - source_family: skills - source_path: .github/skills/internal-aws-strategic - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-azure-devops - source_family: skills - source_path: .github/skills/internal-azure-devops - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-azure-governance - source_family: skills - source_path: .github/skills/internal-azure-governance - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-azure-operations - source_family: skills - source_path: .github/skills/internal-azure-operations - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-azure-organization-structure - source_family: skills - source_path: .github/skills/internal-azure-organization-structure - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-azure-strategic - source_family: skills - source_path: .github/skills/internal-azure-strategic - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-bash - source_family: skills - source_path: .github/skills/internal-bash - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-changelog-automation - source_family: skills - source_path: .github/skills/internal-changelog-automation - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-cloud-policy - source_family: skills - source_path: .github/skills/internal-cloud-policy - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-code-review - source_family: skills - source_path: .github/skills/internal-code-review - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-copilot-audit - source_family: skills - source_path: .github/skills/internal-copilot-audit - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-copilot-docs-research - source_family: skills - source_path: .github/skills/internal-copilot-docs-research - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-ddd - source_family: skills - source_path: .github/skills/internal-ddd - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-debugging - source_family: skills - source_path: .github/skills/internal-debugging - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-devops-core-principles - source_family: skills - source_path: .github/skills/internal-devops-core-principles - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-docker - source_family: skills - source_path: .github/skills/internal-docker - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gateway-critical-master - source_family: skills - source_path: .github/skills/internal-gateway-critical-master - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gateway-review - source_family: skills - source_path: .github/skills/internal-gateway-review - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gateway-simple-task - source_family: skills - source_path: .github/skills/internal-gateway-simple-task - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gcp-governance - source_family: skills - source_path: .github/skills/internal-gcp-governance - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gcp-operations - source_family: skills - source_path: .github/skills/internal-gcp-operations - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gcp-organization-structure - source_family: skills - source_path: .github/skills/internal-gcp-organization-structure - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gcp-strategic - source_family: skills - source_path: .github/skills/internal-gcp-strategic - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-github-action-composite - source_family: skills - source_path: .github/skills/internal-github-action-composite - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-github-actions - source_family: skills - source_path: .github/skills/internal-github-actions - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-github-governance - source_family: skills - source_path: .github/skills/internal-github-governance - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-github-operations - source_family: skills - source_path: .github/skills/internal-github-operations - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-github-pr - source_family: skills - source_path: .github/skills/internal-github-pr - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-github-strategic - source_family: skills - source_path: .github/skills/internal-github-strategic - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-go - source_family: skills - source_path: .github/skills/internal-go - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-high-level-review - source_family: skills - source_path: .github/skills/internal-high-level-review - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gateway-idea-brainstorming - source_family: skills - source_path: .github/skills/internal-gateway-idea-brainstorming - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-java - source_family: skills - source_path: .github/skills/internal-java - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-json - source_family: skills - source_path: .github/skills/internal-json - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-kubernetes - source_family: skills - source_path: .github/skills/internal-kubernetes - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-kubernetes-deployment - source_family: skills - source_path: .github/skills/internal-kubernetes-deployment - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-lesson-codification - source_family: skills - source_path: .github/skills/internal-lesson-codification - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-makefile - source_family: skills - source_path: .github/skills/internal-makefile - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-markdown - source_family: skills - source_path: .github/skills/internal-markdown - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-nodejs - source_family: skills - source_path: .github/skills/internal-nodejs - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-oop-design-patterns - source_family: skills - source_path: .github/skills/internal-oop-design-patterns - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-performance-optimization - source_family: skills - source_path: .github/skills/internal-performance-optimization - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-java-project - source_family: skills - source_path: .github/skills/internal-java-project - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-nodejs-project - source_family: skills - source_path: .github/skills/internal-nodejs-project - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-python-project - source_family: skills - source_path: .github/skills/internal-python-project - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-python - source_family: skills - source_path: .github/skills/internal-python - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-bash-script - source_family: skills - source_path: .github/skills/internal-bash-script - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-python-script - source_family: skills - source_path: .github/skills/internal-python-script - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-skill-creator - source_family: skills - source_path: .github/skills/internal-skill-creator - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-java-spring-boot-development - source_family: skills - source_path: .github/skills/internal-java-spring-boot-development - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-tdd - source_family: skills - source_path: .github/skills/internal-tdd - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-terraform - source_family: skills - source_path: .github/skills/internal-terraform - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-yaml - source_family: skills - source_path: .github/skills/internal-yaml - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: mattpocock-caveman - source_family: skills - source_path: .github/skills/mattpocock-caveman - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: openai-docx - source_family: skills - source_path: .github/skills/openai-docx - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: openai-gh-address-comments - source_family: skills - source_path: .github/skills/openai-gh-address-comments - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: openai-gh-fix-ci - source_family: skills - source_path: .github/skills/openai-gh-fix-ci - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: openai-pdf - source_family: skills - source_path: .github/skills/openai-pdf - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: openai-slides - source_family: skills - source_path: .github/skills/openai-slides - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: openai-spreadsheet - source_family: skills - source_path: .github/skills/openai-spreadsheet - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-brainstorming - source_family: skills - source_path: .github/skills/superpowers-brainstorming - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-dispatching-parallel-agents - source_family: skills - source_path: .github/skills/superpowers-dispatching-parallel-agents - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-executing-plans - source_family: skills - source_path: .github/skills/superpowers-executing-plans - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-finishing-a-development-branch - source_family: skills - source_path: .github/skills/superpowers-finishing-a-development-branch - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-receiving-code-review - source_family: skills - source_path: .github/skills/superpowers-receiving-code-review - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-requesting-code-review - source_family: skills - source_path: .github/skills/superpowers-requesting-code-review - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-subagent-driven-development - source_family: skills - source_path: .github/skills/superpowers-subagent-driven-development - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-systematic-debugging - source_family: skills - source_path: .github/skills/superpowers-systematic-debugging - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-test-driven-development - source_family: skills - source_path: .github/skills/superpowers-test-driven-development - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-using-git-worktrees - source_family: skills - source_path: .github/skills/superpowers-using-git-worktrees - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-using-superpowers - source_family: skills - source_path: .github/skills/superpowers-using-superpowers - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-verification-before-completion - source_family: skills - source_path: .github/skills/superpowers-verification-before-completion - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: superpowers-writing-plans - source_family: skills - source_path: .github/skills/superpowers-writing-plans - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: terraform-terraform-search-import - source_family: skills - source_path: .github/skills/terraform-terraform-search-import - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: terraform-terraform-test - source_family: skills - source_path: .github/skills/terraform-terraform-test - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Auto-included skill bundle. -- resource_id: internal-gateway-idea-brainstorming - source_family: agents - source_path: .github/agents/internal-gateway-idea-brainstorming.agent.md - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Gateway agent for substantive idea definition and brainstorming. -- resource_id: internal-gateway-review - source_family: agents - source_path: .github/agents/internal-gateway-review.agent.md - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Gateway agent for staged workflow orchestration. -- resource_id: internal-gateway-critical-master - source_family: agents - source_path: .github/agents/internal-gateway-critical-master.agent.md - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Gateway agent for critical challenge and pressure testing. -- resource_id: internal-gateway-simple-task - source_family: agents - source_path: .github/agents/internal-gateway-simple-task.agent.md - include_targets: - - codex - - copilot - - opencode - target_support: See runtime support matrix - notes: Gateway agent for simple fast-path tasks. + - resource_id: internal-gateway-idea-brainstorming + source_family: agents + source_path: .github/agents/internal-gateway-idea-brainstorming.agent.md + include_targets: + - codex + - copilot + - opencode + target_support: See runtime support matrix + notes: Gateway agent for substantive idea definition and brainstorming. + - resource_id: internal-gateway-review + source_family: agents + source_path: .github/agents/internal-gateway-review.agent.md + include_targets: + - codex + - copilot + - opencode + target_support: See runtime support matrix + notes: Gateway agent for staged workflow orchestration. + - resource_id: internal-gateway-critical-master + source_family: agents + source_path: .github/agents/internal-gateway-critical-master.agent.md + include_targets: + - codex + - copilot + - opencode + target_support: See runtime support matrix + notes: Gateway agent for critical challenge and pressure testing. + - resource_id: internal-gateway-simple-task + source_family: agents + source_path: .github/agents/internal-gateway-simple-task.agent.md + include_targets: + - codex + - copilot + - opencode + target_support: See runtime support matrix + notes: Gateway agent for simple fast-path tasks. diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/runtime-support-matrix.md b/.github/skills/local-agent-sync-install-ai-resources/references/runtime-support-matrix.md deleted file mode 100644 index bbee4701..00000000 --- a/.github/skills/local-agent-sync-install-ai-resources/references/runtime-support-matrix.md +++ /dev/null @@ -1,20 +0,0 @@ -# Runtime Support Matrix - -Use this reference when the selected runtime or resource family decides whether `plan`, `audit`, `doctor`, or `apply` is allowed. - -## Summary - -| Target | Resource family | Support level | Home path | Direct copy | Translation required | Include in v1 | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | -| `codex` | Skills | Documented | `~/.agents/skills//` | Yes | No | Yes | Direct-copy bundles with `SKILL.md`, `references/`, `scripts/`, `assets/`, and `agents/openai.yaml`. | -| `copilot` | Skills | Documented | `~/.agents/skills//` | Yes | No | Yes | GitHub Copilot supports Agent Skills standard via `~/.agents/skills/`. | -| `opencode` | Skills | Documented | `~/.agents/skills//` | Yes | No | Yes | OpenCode supports Agent Skills standard via `~/.agents/skills/`. | -| `codex` | Agents | Documented | `~/.codex/agents/` | No | Yes | Yes | Requires MD to TOML translation. | -| `copilot` | Agents | Documented | `~/.copilot/agents/` | Yes | No | Yes | Direct copy of `.agent.md` files. | -| `opencode` | Agents | Documented | `~/.config/opencode/agents/` | No | Yes | Yes | Requires frontmatter translation (permission object, handoffs to body). | - -## Default v1 Policy - -- Include only `skills` rows with `support_level: Documented`, `direct_copy_possible: true`, and `include_in_v1: true`. -- Include `agents` rows with `support_level: Documented` and `include_in_v1: true`. For targets where `direct_copy_possible: false` and `translation_required: true`, the sync plan delegates to the agent translation module (`scripts/agent_translation.py`). -- Treat non-`Documented` rows as `plan`, `audit`, and `doctor` only unless the user explicitly enables an experimental run. 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 d36825f1..4e01494e 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 @@ -8,6 +8,7 @@ Use this reference when the paired agent or skill needs the exact sync rules rat - Target roots: runtime home resource directories for supported AI runtimes. - Managed families in v1: allowlisted `skills` and `agents`. - Excluded in v1: non-`skills` and non-`agents` runtime resources and undocumented families. +- Skill resources are normally auto-discovered from `.github/skills/` according to `home-sync-catalog.yaml` defaults. The catalog should list policy defaults and explicit non-skill resources, not serialize every skill bundle. ## State Root @@ -83,6 +84,8 @@ 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. + ### Shared Header Always start with a short status line that includes: @@ -99,6 +102,7 @@ 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. @@ -118,6 +122,8 @@ 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`, `Stopped on`, `Validation`, and `Next`. + After the shared header and summary, show one change-oriented table and one attention table when they are useful. Planned changes table: @@ -162,8 +168,9 @@ Behavior: 2. Stop before writing when install blockers, residual drift, stale managed resources, missing directory creation without `--create-missing-dirs`, or destructive cleanup gates are present. 3. If the install lane is clean, apply repo-to-home materialization and verify hashes plus manifest state. 4. For the default `skills` target, run `bisync plan` after install. -5. Stop and report `needs_review` when bisync reports any drift or blocker. Do not run `bisync apply` automatically. -6. Report `done` only when install succeeded or had no work and bisync has zero drift and zero blockers. +5. Stop and report `needs_review` when bisync reports `home-to-repo`, `only-home`, `equal-mtime`, or another non-safe blocker. Do not run `bisync apply` automatically. +6. Treat `repo-to-home` and `only-repo` bisync entries as safe informational leftovers for default `sync`; report them without stopping the sync run. +7. Report `done` when install succeeded or had no work and bisync has no home-owned or ambiguous drift. Exit behavior: @@ -242,7 +249,7 @@ The `bisync` lane provides explicit bidirectional reconciliation between `.githu ### Logic 1. Scan both directories and collect all skill names (union). -2. Exclude bundles whose name starts with `local-agent-sync-`. +2. Exclude bundles whose name starts with `local-`. 3. For each skill present in both sides: - Compute content hash (excluding `.venv`, `__pycache__`, `.pytest_cache`, `.pyc`, `.pyo`). - If hashes match, the skill is `in-sync` (not reported). @@ -279,7 +286,7 @@ Before any write in `bisync apply`: ### Exclusions - Runtime artifacts: `.venv`, `__pycache__`, `.pytest_cache`, `.pyc`, `.pyo`. -- Bundle prefix: `local-agent-sync-*` bundles are excluded from bisync scanning and copying. +- Bundle prefix: `local-*` bundles are excluded from bisync scanning and copying. ### Output 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 d1c5a044..a753f030 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 @@ -24,7 +24,7 @@ IGNORED_SYNC_PARTS: tuple[str, ...] = (".venv", "__pycache__", ".pytest_cache") IGNORED_SYNC_SUFFIXES: tuple[str, ...] = (".pyc", ".pyo") -EXCLUDED_BUNDLE_PREFIX: str = "local-agent-sync-" +EXCLUDED_BUNDLE_PREFIX: str = "local-" def should_ignore(path: Path) -> bool: @@ -39,12 +39,10 @@ def should_ignore_copytree(directory: str, names: list[str]) -> set[str]: ignored: set[str] = set() for name in names: candidate = base / name - if candidate.is_dir(): - if name in IGNORED_SYNC_PARTS: - ignored.add(name) - elif candidate.is_file(): - if Path(name).suffix in IGNORED_SYNC_SUFFIXES: - ignored.add(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 @@ -538,7 +536,11 @@ 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") - if plan.blocked_codes: + resolvable_during_apply = {"bisync-only-repo"} + remaining_blockers = [ + code for code in plan.blocked_codes if code not in resolvable_during_apply + ] + if remaining_blockers: _emit_bisync_output(plan, args.format) return 1 @@ -562,50 +564,7 @@ def _emit_bisync_output(plan: BisyncPlan, format_name: str) -> None: if format_name == "json": print(json.dumps(payload, indent=2, sort_keys=True)) return - if format_name == "report": - print(render_bisync_report(payload), end="") - return - - if not plan.drifts: - print("\u2705 No drift detected. Source and home are in sync.") - return - - print(f"\u2139\ufe0f Detected {len(plan.drifts)} drift(s):\n") - for drift in plan.drifts: - skill = drift.skill_name - dtype = drift.drift_type - if dtype in ("only-repo", "only-home"): - label = "only in repo" if dtype == "only-repo" else "only in home" - path = drift.repo_path if dtype == "only-repo" else drift.home_path - print(f" {skill}: {label} ({path})") - blocker_codes = list(drift.blocked_codes) - if dtype == "only-repo" and not blocker_codes: - blocker_codes = ["bisync-only-repo"] - elif dtype == "only-home" and not blocker_codes: - blocker_codes = ["bisync-only-home"] - print(f" blocker: {', '.join(blocker_codes)}") - elif dtype == "equal-mtime": - print(f" {skill}: equal mtime (hashes differ)") - print(f" blocker: {', '.join(drift.blocked_codes)}") - else: - print(f" {skill}: {drift.direction}") - if drift.direction == "repo-to-home": - winner, loser = "repo", "home" - else: - winner, loser = "home", "repo" - print(f" winner: {winner}") - print(f" loser: {loser}") - - if plan.blocked_codes: - print(f"\n\u26a0\ufe0f Blocked: {', '.join(plan.blocked_codes)}") - - next_step = plan.next_step - if next_step: - print(f"\n\u2139\ufe0f Next: {next_step}") - - next_action = plan.next_action - if next_action and next_action.get("action") != "done": - print(f" Action: {next_action.get('reason', '')}") + print(render_bisync_report(payload), end="") def build_bisync_parser() -> argparse.ArgumentParser: @@ -623,7 +582,7 @@ def build_bisync_parser() -> argparse.ArgumentParser: plan_parser.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="text", + default="report", help="Output format.", ) apply_parser = subparsers.add_parser("apply", help="Apply bisync resolution") @@ -636,7 +595,7 @@ def build_bisync_parser() -> argparse.ArgumentParser: apply_parser.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="text", + default="report", help="Output format.", ) return parser 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 5b730a36..50120863 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 @@ -70,14 +70,32 @@ def load_runtime_support_matrix(source_root: Path) -> list[RuntimeSupportRow]: def load_home_sync_catalog(source_root: Path) -> list[CatalogResource]: catalog_path = resolve_skill_reference(source_root, HOME_SYNC_CATALOG_PATH) payload = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) or {} - resources = payload.get("resources", []) defaults = payload.get("defaults", {}) include_local = bool(defaults.get("include_local_skills", False)) + include_internal = bool(defaults.get("include_internal_skills", False)) + include_unlisted = bool(defaults.get("include_unlisted_skills", False)) + skill_targets = tuple(defaults.get("skill_targets", ("codex", "copilot", "opencode"))) + resources = list(payload.get("resources", [])) + + if include_unlisted: + explicit_ids = { + resource.get("resource_id", "") + for resource in resources + if isinstance(resource, dict) + } + resources.extend( + resource + for resource in discover_skill_resources(source_root, include_local, include_internal, skill_targets) + if resource["resource_id"] not in explicit_ids + ) + filtered = [] for resource in resources: rid = resource.get("resource_id", "") if not include_local and rid.startswith("local-"): continue + if not include_internal and rid.startswith("internal-"): + continue filtered.append(resource) return [ CatalogResource( @@ -92,6 +110,38 @@ def load_home_sync_catalog(source_root: Path) -> list[CatalogResource]: ] +def discover_skill_resources( + source_root: Path, + include_local: bool, + include_internal: bool, + skill_targets: tuple[str, ...], +) -> list[dict[str, object]]: + skills_root = source_root / ".github" / "skills" + if not skills_root.is_dir(): + return [] + + resources: list[dict[str, object]] = [] + for skill_dir in sorted(path for path in skills_root.iterdir() if path.is_dir()): + resource_id = skill_dir.name + if not include_local and resource_id.startswith("local-"): + continue + if not include_internal and resource_id.startswith("internal-"): + continue + if not (skill_dir / "SKILL.md").is_file(): + continue + resources.append( + { + "resource_id": resource_id, + "source_family": "skills", + "source_path": skill_dir.relative_to(source_root).as_posix(), + "include_targets": list(skill_targets), + "target_support": "See runtime support matrix", + "notes": "Auto-discovered skill bundle.", + } + ) + return resources + + def resolve_skill_reference(source_root: Path, relative_path: Path) -> Path: source_candidate = source_root / SKILL_ROOT_RELATIVE / relative_path if source_candidate.exists(): 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 3918d140..02ca1a63 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 @@ -9,11 +9,12 @@ from datetime import datetime, timezone from pathlib import Path +from agent_translation import target_extension, translate_agent_for_target from home_sync_contract import ( + TARGET_ORDER, CatalogResource, RuntimeSupportRow, has_agent_root, - TARGET_ORDER, load_home_sync_catalog, load_runtime_support_matrix, resolve_support_row, @@ -22,8 +23,6 @@ state_root_for_home, ) -from agent_translation import target_extension, translate_agent_for_target - MANIFEST_PATH = "manifest.json" LAST_PLAN_PATH = "last-plan.json" LAST_AUDIT_PATH = "last-audit.json" @@ -259,6 +258,7 @@ def build_home_sync_plan( operations, target, target_path, + source_path, resource, source_hash, manifest_index, @@ -638,6 +638,7 @@ def add_materialization_operation( operations: list[HomeSyncOperation], target: str, target_path: Path, + source_path: Path, resource: CatalogResource, source_hash: str, manifest_index: dict[str, dict[str, object]], @@ -657,6 +658,19 @@ def add_materialization_operation( ) return if current_hash != manifest_entry.get("content_hash"): + if resource_mtime(target_path) > resource_mtime(source_path): + operations.append( + HomeSyncOperation( + target=target, + action="warning", + path=target_path.as_posix(), + reason="Managed target diverged from the last recorded manifest hash and the home copy is newer than the repo source. Install skips this resource so an explicit home-to-repo review can decide whether to keep or copy back the newer home state.", + code="target-modified-managed", + source_path=resource.source_path, + resource_id=resource.resource_id, + ) + ) + return add_blocked_operation( operations, target=target, @@ -1219,6 +1233,17 @@ def should_ignore_sync_path(path: Path) -> bool: return path.suffix in IGNORED_SYNC_SUFFIXES +def resource_mtime(path: Path) -> float: + if path.is_file(): + return path.stat().st_mtime + + max_time = path.stat().st_mtime if path.exists() else 0.0 + for candidate in path.rglob("*"): + if candidate.is_file() and not should_ignore_sync_path(candidate): + max_time = max(max_time, candidate.stat().st_mtime) + return max_time + + def is_relative_to(path: Path, other: Path) -> bool: try: path.relative_to(other) @@ -1278,7 +1303,7 @@ def next_action_for_plan( "allowed": False, "requires_explicit_approval": True, "command": "", - "reason": f"Blocked codes prevent apply. Resolve each blocker manually.", + "reason": "Blocked codes prevent apply. Resolve each blocker manually.", } if missing_dirs and mode != "apply": return { 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 ab89c223..e2a00694 100644 --- 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,8 +2,8 @@ # # Purpose: Bootstrap this skill's local Python environment and run the home AI resource sync tool. # Usage examples: -# ./scripts/run.sh plan --targets codex,copilot,opencode -# ./scripts/run.sh apply --targets codex --create-missing-dirs +# ./scripts/run.sh sync --targets skills --format report +# ./scripts/run.sh apply --targets codex --create-missing-dirs --format report set -Eeuo pipefail 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 7bd60b76..9d6c1fee 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 plan --targets skills - python3 scripts/sync_home_ai_resources.py apply --targets skills --create-missing-dirs - python3 scripts/sync_home_ai_resources.py apply --targets skills,codex --retire-targets opencode --prune-managed + 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 """ from __future__ import annotations @@ -14,7 +14,6 @@ from pathlib import Path from bisync_skills import ( - apply_bisync_plan, build_bisync_plan, run_bisync_apply, run_bisync_plan, @@ -28,8 +27,13 @@ write_doctor_snapshot, write_plan_snapshot, ) -from sync_output import build_compact_install_output, render_install_report -from sync_output import build_compact_bisync_output, render_bisync_report +from sync_output import ( + build_compact_bisync_output, + build_compact_install_output, + render_doctor_report, + render_install_report, + render_sync_report, +) def parse_args(argv: list[str] | None = None) -> argparse.Namespace: @@ -76,7 +80,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: cmd_parser.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="text", + default="report", help="Output format.", ) cmd_parser.add_argument( @@ -102,7 +106,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: bisync_plan.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="text", + default="report", help="Output format.", ) bisync_apply = bisync_sub.add_parser("apply", help="Apply bisync resolution.") @@ -115,7 +119,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: bisync_apply.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="text", + default="report", help="Output format.", ) @@ -146,6 +150,7 @@ def run(args: argparse.Namespace) -> int: "retired_targets": [], "blocked_codes": ["unknown-target"], "error": str(error), + **blocked_report_fields(str(error)), } emit_output(payload, format_name=args.format, failure_message=str(error)) return 1 @@ -162,6 +167,7 @@ def run(args: argparse.Namespace) -> int: "retired_targets": list(retire_targets), "blocked_codes": ["retire-target-overlap"], "error": message, + **blocked_report_fields(message), } emit_output(payload, format_name=args.format, failure_message=message) return 1 @@ -189,6 +195,9 @@ def run(args: argparse.Namespace) -> int: "selected_targets": list(targets), "checks": checks, "blocked_codes": blocked_codes, + "validation": "blocked" if blocked_codes else "ready", + "next_step": next_step_for_doctor(blocked_codes), + "next_action": next_action_for_doctor(blocked_codes), "state_path": snapshot_path.as_posix(), } emit_output(payload, format_name=args.format) @@ -283,12 +292,12 @@ def run_sync( if targets == ("skills",) and not retire_targets: bisync_plan = build_bisync_plan(source_root, home_root, mode="plan") bisync_payload = bisync_plan.to_dict() - if bisync_plan.blocked_codes or bisync_plan.drifts: + if bisync_requires_review(bisync_plan): emit_sync_output( { "mode": "sync", "status": "needs_review", - "reason": "Bisync drift or blockers need a human direction decision before writes.", + "reason": "Bisync detected home-owned or ambiguous drift that needs human review before sync can finish.", "install": install_payload, "bisync": bisync_payload, }, @@ -300,7 +309,7 @@ def run_sync( { "mode": "sync", "status": "done", - "reason": "Repo-to-home install completed and no bisync drift needs review.", + "reason": "Repo-to-home install completed and no home-owned bisync drift needs review.", "install": install_payload, "bisync": bisync_payload, }, @@ -309,12 +318,34 @@ def run_sync( return 0 +def bisync_requires_review(plan) -> bool: + safe_blocked_codes = {"bisync-only-repo"} + if any(code not in safe_blocked_codes for code in plan.blocked_codes): + return True + + for drift in plan.drifts: + if drift.drift_type == "only-repo": + continue + if drift.drift_type == "drift" and drift.direction == "repo-to-home": + continue + return True + + return False + + def install_auto_apply_blockers( plan, args: argparse.Namespace, ) -> list[str]: blockers = list(plan.blocked_codes()) - if plan.residual_drift: + if any( + operation.action in {"blocked", "stale-managed"} + or ( + operation.action == "warning" + and operation.code != "target-modified-managed" + ) + for operation in plan.operations + ): blockers.append("install-residual-drift") if any(operation.action == "mkdir" for operation in plan.operations) and not args.create_missing_dirs: blockers.append("needs-directory-create") @@ -342,17 +373,7 @@ def emit_sync_output(payload: dict[str, object], *, format_name: str) -> None: print(json.dumps(compact, indent=2, sort_keys=True)) return - status = str(payload.get("status") or "unknown") - reason = str(payload.get("reason") or "") - print(f"Status: mode=sync; status={status}; reason={reason}") - print("") - if isinstance(install_payload, dict): - print("# Install Lane") - print(render_install_report(install_payload), end="") - if isinstance(bisync_payload, dict): - print("") - print("# Bisync Lane") - print(render_bisync_report(bisync_payload), end="") + print(render_sync_report(payload), end="") def run_apply(plan, args: argparse.Namespace) -> int: @@ -373,10 +394,47 @@ def run_apply(plan, args: argparse.Namespace) -> int: payload["manifest_path"] = manifest_path.as_posix() payload["state_path"] = snapshot_path.as_posix() emit_output(payload, format_name=args.format) - log_success("Home AI resource apply completed.") return 0 +def next_step_for_doctor(blocked_codes: list[str]) -> str: + if blocked_codes: + return "Resolve the readiness blockers, then rerun doctor before applying home sync changes." + return "Readiness checks passed. Run plan or sync when ready." + + +def next_action_for_doctor(blocked_codes: list[str]) -> dict[str, object]: + if blocked_codes: + return { + "action": "resolve_blockers", + "allowed": False, + "requires_explicit_approval": True, + "command": "none", + "reason": "Doctor found readiness blockers that must be resolved before apply.", + } + return { + "action": "plan", + "allowed": True, + "requires_explicit_approval": False, + "command": "plan --targets --format report", + "reason": "Doctor found no readiness blockers.", + } + + +def blocked_report_fields(reason: str) -> dict[str, object]: + return { + "validation": "blocked", + "next_step": "Resolve the reported blocker, then rerun the same command.", + "next_action": { + "action": "resolve_blockers", + "allowed": False, + "requires_explicit_approval": True, + "command": "none", + "reason": reason, + }, + } + + def normalize_mode(command: str) -> str: return "plan" if command == "dry-run" else command @@ -398,103 +456,10 @@ def emit_output( print(json.dumps(payload, indent=2, sort_keys=True)) return - if format_name == "report": - print(render_install_report(payload), end="") + if payload.get("mode") == "doctor": + print(render_doctor_report(payload), end="") return - - mode = payload.get("mode", "plan") - targets = ", ".join(payload.get("selected_targets", [])) or "none" - retired_targets = ", ".join(payload.get("retired_targets", [])) - log_info(f"Mode: {mode}") - log_info(f"Targets: {targets}") - if retired_targets: - log_info(f"Retire targets: {retired_targets}") - - operations = payload.get("operations", []) - copied_ops = [op for op in operations if isinstance(op, dict) and op.get("action") == "copy"] - skipped_ops = [op for op in operations if isinstance(op, dict) and op.get("action") == "skip"] - blocked_ops = [op for op in operations if isinstance(op, dict) and op.get("action") == "blocked"] - stale_ops = [op for op in operations if isinstance(op, dict) and op.get("action") == "stale-managed"] - mkdir_ops = [op for op in operations if isinstance(op, dict) and op.get("action") == "mkdir"] - source_resources = payload.get("source_resources_considered") - - log_info( - f"Summary: {len(copied_ops)} to copy, {len(skipped_ops)} up-to-date, " - f"{len(blocked_ops)} blocked" - + (f", {len(stale_ops)} stale" if stale_ops else "") - + (f" ({source_resources} resources considered)" if isinstance(source_resources, int) else "") - ) - - if copied_ops: - copied_resources: dict[str, dict[str, list[str]]] = {} - for op in copied_ops: - rid = op.get("resource_id", "unknown") - target = op.get("target", "?") - path = op.get("path", "") - family = "agents" if "/agents/" in path else "skills" - if rid not in copied_resources: - copied_resources[rid] = {"skills": [], "agents": []} - copied_resources[rid][family].append(target) - for rid, families in sorted(copied_resources.items()): - parts = [] - for family in ("skills", "agents"): - if families[family]: - parts.append(f"{family}→{','.join(families[family])}") - log_info(f" + {rid} ({'; '.join(parts)})") - - blocked_codes = payload.get("blocked_codes", []) - if blocked_codes: - log_error(f"Blocked codes: {', '.join(blocked_codes)}") - blocked_by_code: dict[str, list[dict]] = {} - for op in blocked_ops: - code = op.get("code", "unknown") - blocked_by_code.setdefault(code, []).append(op) - for code, ops in sorted(blocked_by_code.items()): - log_error(f" [{code}] {len(ops)} path(s)") - for op in ops: - path = op.get("path", "") - reason = op.get("reason", "") - if reason: - log_error(f" - {path}: {reason}") - else: - log_error(f" - {path}") - - if stale_ops: - log_info(f"Stale managed resources: {len(stale_ops)}") - for op in stale_ops: - path = op.get("path", "") - reason = op.get("reason", "") - code = op.get("code", "") - if code: - log_info(f" ~ {path} [{code}]: {reason}") - else: - log_info(f" ~ {path}: {reason}") - - for path in payload.get("missing_dirs", []): - log_info(f"Missing dir: {path}") - - residual_drift = payload.get("residual_drift", []) - if residual_drift: - log_info(f"Residual drift: {len(residual_drift)} path(s)") - - validation = payload.get("validation") - if isinstance(validation, str): - log_info(f"Validation: {validation}") - - next_step = payload.get("next_step") - if isinstance(next_step, str) and next_step: - log_info(f"Next: {next_step}") - - state_path = payload.get("state_path") - if isinstance(state_path, str): - log_info(f"State: {state_path}") - - manifest_path = payload.get("manifest_path") - if isinstance(manifest_path, str): - log_info(f"Manifest: {manifest_path}") - - if failure_message: - log_error(failure_message) + print(render_install_report(payload), end="") def find_repo_root(start: Path) -> Path: @@ -505,17 +470,5 @@ def find_repo_root(start: Path) -> Path: raise FileNotFoundError(f"Unable to find repository root from {start}") -def log_info(message: str) -> None: - print(f"ℹ️ {message}", flush=True) - - -def log_success(message: str) -> None: - print(f"✅ {message}", flush=True) - - -def log_error(message: str) -> None: - print(f"❌ {message}", flush=True) - - if __name__ == "__main__": raise SystemExit(main()) 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 554aa648..85100dd7 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 @@ -2,7 +2,6 @@ from collections.abc import Iterable - BLOCKER_REASON_MAP: dict[str, str] = { "unknown-target": "The selected target is not supported.", "unsupported-family": "The selected resource family is not supported for this target.", @@ -35,6 +34,8 @@ "bisync-residual-drift": "Post-apply bisync still detected residual drift.", } +REPORT_TABLE_ROW_LIMIT = 20 + def _install_lane_label() -> str: return "repo-to-home install" @@ -95,6 +96,86 @@ def build_compact_bisync_output(payload: dict[str, object]) -> dict[str, object] return compact +def render_sync_report(payload: dict[str, object]) -> str: + status = str(payload.get("status") or "unknown") + reason = str(payload.get("reason") or "") + install_payload = payload.get("install") if isinstance(payload.get("install"), dict) else {} + bisync_payload = payload.get("bisync") if isinstance(payload.get("bisync"), dict) else {} + targets = _join_or_none(_as_string_list(install_payload.get("selected_targets"))) + next_action = _sync_next_action(payload) + + install_operations = install_payload.get("operations") if isinstance(install_payload, dict) else None + direction_counts = _direction_counts(bisync_payload.get("drifts")) if isinstance(bisync_payload, dict) else _direction_counts(()) + bucket_counts = _bucket_counts(bisync_payload.get("drifts")) if isinstance(bisync_payload, dict) else _bucket_counts(()) + + lines: list[str] = [] + lines.append( + f"Status: sync | status={status} | targets={targets} | next_action={next_action} | reason={reason}" + ) + lines.append("") + + lines.extend(_report_section("Summary")) + lines.extend( + _bullet_lines( + [ + f"Install copied: {_count_operations(install_operations, {'copy'})}", + f"Install skipped: {_count_operations(install_operations, {'skip'})}", + f"Install warnings: {_count_operations(install_operations, {'warning'})}", + f"Install blockers: {_count_operations(install_operations, {'blocked', 'stale-managed'})}", + f"Bisync repo-to-home: {direction_counts['repo_to_home']}", + f"Bisync home-to-repo: {direction_counts['home_to_repo']}", + f"Bisync only-repo: {bucket_counts['only_repo']}", + f"Bisync only-home: {bucket_counts['only_home']}", + f"Bisync equal-mtime: {bucket_counts['equal_mtime']}", + ] + ) + ) + lines.append("") + + lines.extend(_report_section("Auto-applied")) + lines.extend( + _table_lines( + ["Skill or path", "Why copied", "Verification"], + _bounded_rows( + _sync_auto_applied_rows(install_payload if isinstance(install_payload, dict) else {}), + "auto-applied", + ), + none_row=["none", "No safe repo-to-home copy was applied.", "none"], + ) + ) + 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."], + ) + ) + lines.append("") + + lines.extend(_report_section("Validation")) + lines.extend( + _table_lines( + ["Check", "Result"], + _sync_validation_rows( + status, + install_payload if isinstance(install_payload, dict) else {}, + bisync_payload if isinstance(bisync_payload, dict) else {}, + ), + ) + ) + lines.append("") + + lines.extend(_report_section("Next")) + lines.extend(_table_lines(["Field", "Value"], _sync_next_rows(payload))) + return "\n".join(lines).strip() + "\n" + + def _copy_if_present( target: dict[str, object], source: dict[str, object], @@ -217,21 +298,148 @@ def _bisync_change_evidence(drifts: object, limit: int = 8) -> list[dict[str, ob return evidence +def _sync_auto_applied_rows(install_payload: dict[str, object]) -> list[list[str]]: + rows: list[list[str]] = [] + operations = install_payload.get("operations") + if not isinstance(operations, list): + return rows + manifest_path = install_payload.get("manifest_path") + verification = "manifest hash recorded" if isinstance(manifest_path, str) and manifest_path else "planned only" + for operation in operations: + if not isinstance(operation, dict): + continue + if str(operation.get("action") or "") != "copy": + 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]) + return rows + + +def _sync_stopped_rows( + install_payload: dict[str, object], + bisync_payload: dict[str, object], +) -> list[list[str]]: + rows: list[list[str]] = [] + + operations = install_payload.get("operations") + if isinstance(operations, list): + for operation in operations: + if not isinstance(operation, dict): + continue + action = str(operation.get("action") or "") + code = str(operation.get("code") or "") + if action not in {"blocked", "stale-managed"} and not ( + action == "warning" and code == "target-modified-managed" + ): + continue + path = str(operation.get("resource_id") or operation.get("path") or "unknown") + direction = "install review" + if code == "target-modified-managed": + direction = "home-to-repo review" + reason = str(operation.get("reason") or _reason_for_code(code or action)) + rows.append([path, direction, reason]) + + drifts = bisync_payload.get("drifts") + if isinstance(drifts, list): + for drift in drifts: + if not isinstance(drift, dict): + continue + drift_type = str(drift.get("type") or "drift") + direction = str(drift.get("direction") or drift_type) + if drift_type == "only-repo": + continue + if drift_type == "drift" and direction == "repo-to-home": + continue + skill = str(drift.get("skill") or "unknown") + rows.append([skill, direction, _sync_drift_summary(drift)]) + return rows + + +def _sync_validation_rows( + status: str, + install_payload: dict[str, object], + bisync_payload: dict[str, object], +) -> list[list[str]]: + rows = [["Overall sync", status]] + install_status = str(install_payload.get("validation") or install_payload.get("status") or "not-run") + rows.append(["Install lane", install_status]) + + bisync_status = "not-run" + verification = bisync_payload.get("verification") + if isinstance(verification, dict): + bisync_status = str(verification.get("status") or bisync_status) + elif bisync_payload: + bisync_status = "reviewed" + rows.append(["Bisync lane", bisync_status]) + + state_path = install_payload.get("state_path") + if isinstance(state_path, str) and state_path: + rows.append(["State path", state_path]) + manifest_path = install_payload.get("manifest_path") + if isinstance(manifest_path, str) and manifest_path: + rows.append(["Manifest path", manifest_path]) + return rows + + +def _sync_next_rows(payload: dict[str, object]) -> list[list[str]]: + action = _sync_next_action(payload) + status = str(payload.get("status") or "unknown") + reason = str(payload.get("reason") or "Review the latest sync report.") + bisync_payload = payload.get("bisync") if isinstance(payload.get("bisync"), dict) else {} + + if status == "done": + return [["Action", action], ["Reason", reason]] + + if isinstance(bisync_payload, dict) and _sync_stopped_rows({}, bisync_payload): + return [ + ["Action", action], + ["Reason", "Review home-owned or ambiguous drift, then run explicit `bisync plan` or `bisync apply` if needed."], + ] + + return [ + ["Action", action], + ["Reason", "Resolve install blockers or warnings, then rerun `sync`."], + ] + + +def _sync_next_action(payload: dict[str, object]) -> str: + status = str(payload.get("status") or "unknown") + bisync_payload = payload.get("bisync") if isinstance(payload.get("bisync"), dict) else {} + if status == "done": + return "done" + if isinstance(bisync_payload, dict) and _sync_stopped_rows({}, bisync_payload): + return "review_bisync" + return "review_install" + + +def _sync_drift_summary(drift: dict[str, object]) -> str: + drift_type = str(drift.get("type") or "drift") + direction = str(drift.get("direction") or drift_type) + if drift_type == "only-home": + return "Only present in home. Manual review decides whether to keep, remove, or add it to the repo." + if drift_type == "equal-mtime": + return "Repo and home hashes differ, but timestamps are equal, so the winner is ambiguous." + if direction == "home-to-repo": + return "Home copy is newer than repo copy and needs an explicit home-to-repo decision." + return _bisync_reason(drift_type, direction) + + def render_install_report(payload: dict[str, object]) -> str: mode = str(payload.get("mode") or "plan") targets = _as_string_list(payload.get("selected_targets")) blocked_codes = _as_string_list(payload.get("blocked_codes")) status = str(payload.get("validation") or payload.get("status") or "unknown") + next_action = _next_action_name(payload.get("next_action")) lane_label = _install_lane_label() operations = payload.get("operations") - copied_count = _count_operations(operations, {"copy"}) skipped_count = _count_operations(operations, {"skip"}) blocked_count = _count_operations(operations, {"blocked"}) planned_count = _count_operations(operations, {"copy", "mkdir", "delete", "stale-managed"}) lines: list[str] = [] lines.append( - f"Status: {lane_label} | mode={mode} | status={status} | blockers={len(blocked_codes)}" + f"Status: {lane_label} | mode={mode} | targets={_join_or_none(targets)} | status={status} | blockers={len(blocked_codes)} | next_action={next_action}" ) lines.append("") @@ -250,7 +458,7 @@ def render_install_report(payload: dict[str, object]) -> str: ) lines.append("") - planned_rows = _install_planned_rows(payload) + planned_rows = _bounded_rows(_install_planned_rows(payload), "change") lines.extend(_report_section("Changes")) lines.extend( _table_lines( @@ -261,7 +469,7 @@ def render_install_report(payload: dict[str, object]) -> str: ) lines.append("") - completed_rows = _install_completed_rows(payload) + completed_rows = _bounded_rows(_install_completed_rows(payload), "completed action") if completed_rows: lines.append("") lines.extend(_report_section("Completed")) @@ -311,6 +519,57 @@ def render_install_report(payload: dict[str, object]) -> str: return "\n".join(lines).strip() + "\n" +def render_doctor_report(payload: dict[str, object]) -> str: + targets = _as_string_list(payload.get("selected_targets")) + blocked_codes = _as_string_list(payload.get("blocked_codes")) + status = str(payload.get("validation") or payload.get("status") or "unknown") + next_action = _next_action_name(payload.get("next_action")) + checks = payload.get("checks") + lane_label = _install_lane_label() + + 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}" + ) + lines.append("") + + lines.extend(_report_section("Summary")) + lines.extend( + _bullet_lines( + [ + f"Targets: {_join_or_none(targets)}", + f"Readiness checks: {_count_value(checks)}", + f"Ready checks: {_doctor_check_count(checks, 'ok')}", + f"Warnings: {_doctor_check_count(checks, 'warning')}", + f"Blocked checks: {_doctor_check_count(checks, 'blocked')}", + ] + ) + ) + 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."], + ) + ) + lines.append("") + + lines.extend(_report_section("Validation")) + validation_rows = [["Validation status", status]] + state_path = payload.get("state_path") + if isinstance(state_path, str) and state_path: + validation_rows.append(["State path", state_path]) + lines.extend(_table_lines(["Check", "Result"], validation_rows)) + lines.append("") + + lines.extend(_report_section("Next")) + lines.extend(_next_action_table(payload.get("next_action"), payload.get("next_step"))) + return "\n".join(lines).strip() + "\n" + + def render_bisync_report(payload: dict[str, object]) -> str: mode = str(payload.get("mode") or "plan") blocked_codes = _as_string_list(payload.get("blocked_codes")) @@ -320,11 +579,12 @@ def render_bisync_report(payload: dict[str, object]) -> str: status = "unknown" if isinstance(verification, dict): status = str(verification.get("status") or status) + next_action = _next_action_name(payload.get("next_action")) lane_label = _bisync_lane_label() lines: list[str] = [] lines.append( - f"Status: {lane_label} | mode={mode} | status={status} | drift_total={drift_total} | blockers={len(blocked_codes)}" + f"Status: {lane_label} | mode={mode} | target=skills | status={status} | drift_total={drift_total} | blockers={len(blocked_codes)} | next_action={next_action}" ) lines.append("") @@ -348,12 +608,12 @@ def render_bisync_report(payload: dict[str, object]) -> str: lines.extend( _table_lines( ["Resource or path", "Lane", "Planned action", "Why this will change", "Evidence or winner"], - _bisync_planned_rows(payload), + _bounded_rows(_bisync_planned_rows(payload), "change"), none_row=["none", "bisync", "no-op", "No drift detected.", "none"], ) ) - completed_rows = _bisync_completed_rows(payload) + completed_rows = _bounded_rows(_bisync_completed_rows(payload), "completed action") if completed_rows: lines.append("") lines.extend(_report_section("Completed")) @@ -428,15 +688,31 @@ def _table_lines( *, none_row: list[str] | None = None, ) -> list[str]: - output = ["| " + " | ".join(headers) + " |", "| " + " | ".join(["---"] * len(headers)) + " |"] + output = [ + "| " + " | ".join(_table_cell(header) for header in headers) + " |", + "| " + " | ".join(["---"] * len(headers)) + " |", + ] if not rows and none_row is not None: rows = [none_row] for row in rows: padded = row + [""] * (len(headers) - len(row)) - output.append("| " + " | ".join(padded[: len(headers)]) + " |") + output.append("| " + " | ".join(_table_cell(cell) for cell in padded[: len(headers)]) + " |") return output +def _table_cell(value: object) -> str: + return " ".join(str(value).splitlines()).replace("|", r"\|") + + +def _bounded_rows(rows: list[list[str]], label: str) -> list[list[str]]: + if len(rows) <= REPORT_TABLE_ROW_LIMIT: + return rows + omitted_count = len(rows) - REPORT_TABLE_ROW_LIMIT + return rows[:REPORT_TABLE_ROW_LIMIT] + [ + [f"{omitted_count} additional {label} rows omitted; use --format json for full detail."] + ] + + def _join_or_none(values: list[str]) -> str: if not values: return "none" @@ -552,6 +828,52 @@ def _bisync_completed_rows(payload: dict[str, object]) -> list[list[str]]: return rows +def _doctor_check_count(checks: object, status: str) -> int: + if not isinstance(checks, list): + return 0 + return sum(1 for check in checks if isinstance(check, dict) and check.get("status") == status) + + +def _doctor_readiness_rows(payload: dict[str, object]) -> list[list[str]]: + checks = payload.get("checks") + if not isinstance(checks, list): + return [] + + rows: list[list[str]] = [] + for check in checks: + if not isinstance(check, dict): + continue + status = str(check.get("status") or "unknown") + if status == "ok": + continue + code = str(check.get("code") or "none") + name = str(check.get("name") or check.get("path") or "unknown") + path = str(check.get("path") or "") + check_or_path = f"{name} ({path})" if path else name + reason = str(check.get("reason") or _reason_for_code(code)) + blocking_reason = _reason_for_code(code) if code != "none" else "Manual review required." + rows.append([ + check_or_path, + status, + reason, + f"{code}: {blocking_reason}", + _doctor_recommended_action(code), + ]) + return rows + + +def _doctor_recommended_action(code: str) -> str: + if code == "needs-directory-create": + return "Create the runtime root intentionally or rerun apply with --create-missing-dirs after review." + if code == "docs-unverified": + return "Verify runtime support, update the support matrix, or rerun with --experimental-targets only after review." + if code == "source-missing": + return "Fix the catalog entry or restore the missing source path, then rerun doctor." + if code == "unsupported-family": + return "Remove the target or add documented runtime support before apply." + return "Resolve the readiness issue, then rerun doctor." + + def _bisync_blocker_rows(payload: dict[str, object]) -> list[list[str]]: rows: list[list[str]] = [] drifts = payload.get("drifts") @@ -605,6 +927,14 @@ def _next_action_table(next_action: object, next_step: object) -> list[str]: return _table_lines(["Field", "Value"], rows) +def _next_action_name(next_action: object) -> str: + if isinstance(next_action, dict): + action = next_action.get("action") + if isinstance(action, str) and action: + return action + return "unknown" + + def _reason_for_code(code: str) -> str: return BLOCKER_REASON_MAP.get(code, "Manual review required.") diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5058b212..eb996c26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,7 +65,7 @@ repos: # Python linting and formatting - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 22f0422809455ec89ffdcf5a00170ba816e42ddb # ruff-pre-commit v0.15.11; source https://github.com/astral-sh/ruff-pre-commit/releases/tag/v0.15.11 + rev: 3b3f7c3f57fe9925356faf5fe6230835138be230 # ruff-pre-commit v0.15.11; source https://github.com/astral-sh/ruff-pre-commit/releases/tag/v0.15.11 hooks: - id: ruff-check # Auto-sort Python imports with Ruff, replacing isort. name: ruff-check-imports diff --git a/.vscode/settings.json b/.vscode/settings.json index 1356beb7..273f5fb0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "github.copilot.chat.codeGeneration.useInstructionFiles": false, "chat.instructionsFilesLocations": { ".github/instructions": false - } + }, + "python-envs.defaultEnvManager": "ms-python.python:pyenv" } diff --git a/AGENTS.md b/AGENTS.md index 2f808b55..788cd1ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,90 +1,127 @@ -# AGENTS.md - Skill-First AI Configuration Bridge +# AGENTS.md - Repository Operating Core -This file is the repository strategic operating bridge for AI configuration. -Keep it compact, stable, and free of volatile inventory or surface-specific -playbooks. +`AGENTS.md` is the primary always-on repository policy entrypoint for coding +agents in this repository. Keep it compact: it should route agents to the +nearest owner, avoid duplicated guidance, and require explicit validation. `` -This block defines guidance intended to travel as the shared baseline when this -repository projects AI configuration into other repositories. +This block is the portable baseline used when this repository projects AI +configuration into consumer repositories. -## Rule Placement +## First Move -- `AGENTS.md` must define stable repository-wide policy, precedence, tactical operating defaults, ownership boundaries, and routing anchors only. -- Do not put operational procedures, checklists, file-shape recipes, command playbooks, or tool-specific workflows here. -- Put each rule in the smallest valid owner: skills for technical baselines and workflows, agents for route UX, docs for context, validators for enforceable checks, sync owners for propagation, and owned files for their local editing rules. -- An external tool that reads this file may require a clearly delimited, trailing routing block carrying the minimal commands it needs to consume the repository; keep any such block source-local, outside the governed policy blocks, and limited to that tool's required invocation anchors. +- Identify the requested target and nearest owner before broad reading. +- Read only the evidence needed to choose the smallest valid change and check. +- Prefer the closest executable validation; report any validation gap explicitly. -## Writing Style +## Purpose -- Use Plain Technical English for repository-owned prose. -- Prefer short sentences, stable terms, active voice, and explicit `must`, `should`, and `may` wording. -- Keep required technical names unchanged, including paths, commands, schema fields, product names, asset names, and established repository terms. -- Do not rewrite imported upstream text that must remain verbatim. +- Orient coding agents before they read narrower files. +- Keep stable policy separate from volatile inventory; `.github/INVENTORY.md` + owns the exact live catalog of managed AI assets. +- Keep architecture and local context in `docs/architecture.md` and + `docs/repository-context.md`. -## Role And Precedence +## Precedence -- `AGENTS.md` owns repository-wide defaults, precedence, rule placement, and cross-surface bridge behavior. -- `.github/copilot-instructions.md` is the compact repo-wide Copilot bridge; keep it aligned as a routing layer rather than a policy duplicate. -- `.github/INVENTORY.md` owns the exact live catalog of managed AI assets; do not replace it with `AGENTS.md`. -- Sync agents own catalog prefix rules, imported-resource posture, and consumer propagation boundaries. -- `.github/skills/` owns repository-owned reusable guidance. Umbrella skills own lightweight technical-domain baselines; specialist skills and bundle references own deeper procedures. -- Use `docs/architecture.md` for repository architecture and `docs/repository-context.md` for non-policy local context. -- Use relevant skills for workflow depth, runtime consumption behavior, and reusable operating procedures. -- Consumer repositories should express local exceptions with preserved `.github/instructions/local-*.instructions.md` files. +- Direct user instructions win for the current task unless they require unsafe, + destructive, or impossible behavior. +- Resolve conflicts with the smallest valid owner. Treat broader files as + fallback policy, not permission to override narrower contracts. +- Do not infer active policy from removed files, generated output, historical + aliases, or past automation unless it exists on disk and is deliberately + reintroduced. -## Context And Scope +## Operating Principles -- For runtimes without native skill loading, select the smallest relevant skill from the prompt, target path, command surface, validation signal, or repository evidence, then read that `SKILL.md` as manual context. -- Load umbrella domain skills before specialist depth when the domain is clear but the workflow depth is not yet proven. -- Co-load specialist skills or bundle references only when the task needs workflow, decision trees, domain depth, or reusable procedures. -- Use the smallest valid owner to resolve conflicts; file-owned rules and narrower skill contracts win over broad defaults inside their own scope. -- Treat `.github/agents/*.agent.md` as surface-specific wrapper projections; reusable behavior stays in skills when a runtime lacks wrapper UI. -- If no target path is known, infer obvious domains only, such as Python, GitHub Actions, Kubernetes, Docker, Markdown, or Terraform; otherwise ask for the target path before making path-scoped claims. +- Think before acting. Confirm target, nearest owner, bounded evidence, and + validation path before broad commands. +- Make surgical changes. Preserve user work, avoid unrelated refactors, and tie + each edit to the requested outcome. +- Work toward verified outcomes. Run the closest available validation and report + explicit gaps. -## Authoring Defaults +## Scope And Placement -- The default authoring language for repository artifacts is English unless a narrower owned file, skill, or local exception explicitly overrides it. -- User chat may be Italian; repository-owned retained plans may use Italian when the retained-plan policy applies. -- Keep repository-owned AI configuration files as Markdown. Use XML only as runtime prompt-assembly delimiters, never as source format. -- XML-style code-span markers may delimit scope blocks in Markdown AI configuration when the content remains Markdown and the markers clarify runtime, sync, or locality boundaries. -- Do not modify `README.md` files unless the user explicitly asks. -- For vendor-owned or schema-driven surfaces, read primary documentation when correctness depends on platform semantics. -- Update validators, tests, sync discovery, or non-README technical docs when a contract, catalog family, or shared runtime behavior changes. +- `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 duplicate skill-owned paths, templates, workflow states, or command + examples in this file. +- Keep volatile inventory out of this file; link to `.github/INVENTORY.md` + instead of copying catalog entries. +- Target repositories should express local exceptions in their own nearest valid + owner files. -## Operational Ownership +## Authoring Defaults -- Light emoji markers may appear in user-facing macro-category headings when the owning skill defines them; do not use them in paths, commands, identifiers, schema fields, or copied technical values. +- Use Plain Technical English for repository-owned prose. +- Prefer short sentences, stable terms, active voice, and explicit `must`, + `should`, and `may` wording. +- Keep required technical names unchanged, including paths, commands, schema + fields, product names, asset names, and established repository terms. +- The default authoring language for repository artifacts is English unless a + narrower owned file, skill, or local exception explicitly overrides it. +- User chat may use the user's language; repository-owned artifacts follow the + language rules of their closest owner. +- Keep repository-owned AI configuration files as Markdown. Use XML only as + runtime prompt-assembly delimiters, never as source format. +- XML-style code-span markers may delimit Markdown scope blocks when they clarify + runtime, sync, or locality boundaries. +- Do not rewrite imported upstream text that must remain verbatim. +- For vendor-owned or schema-driven surfaces, read primary documentation when + correctness depends on platform semantics. +- Update validators, tests, sync discovery, or non-README technical docs when a + contract, catalog family, or shared runtime behavior changes. ## Tactical Defaults -- Preserve compact working state across turns; avoid rebuilding full context unless new evidence invalidates the current state. -- Keep one active primary owner per execution lane. Load referenced skills on demand based on target, runtime, ownership, and validation path. -- Use bounded evidence: inspect changed sections and failing-validator context first, then expand only when gaps remain. -- Name the validation path early and treat missing validator evidence as a non-`DONE` state until the gap is explicitly resolved. +- Preserve compact working state across turns; avoid rebuilding full context + unless new evidence invalidates the current state. +- Keep one active primary owner per execution lane. Load referenced guidance on + demand based on target, runtime, ownership, and validation path. +- Use bounded evidence: inspect changed sections and failing-validator context + first, then expand only when gaps remain. +- Name the validation path early and treat missing validator evidence as a + non-`DONE` state until the gap is explicitly resolved. ## Delivery And Validation -- Use least privilege and never hardcode secrets, credentials, keys, tokens, or sensitive tenant values. -- Reason from repository evidence. Do not invent runtimes, validators, sync flows, or tests. +- Use least privilege and never hardcode secrets, credentials, keys, tokens, or + sensitive tenant values. +- Reason from repository evidence. Do not invent runtimes, validators, sync + flows, or tests. - Prefer the simplest correct change with the smallest credible blast radius. -- For non-trivial repository-owned work, make target state, anti-scope, assumptions, tradeoffs, and validation path visible before delivery or handoff. -- Run the applicable validation that exists for changed files; when no dedicated validator exists, report the gap and use the closest check. -- Do not treat removed validators, sync scripts, contract tests, or historical aliases as active policy unless they exist on disk and are deliberately reintroduced. +- For non-trivial repository-owned work, make target state, anti-scope, + assumptions, tradeoffs, and validation path visible before delivery or + handoff. +- Run the applicable validation that exists for changed files. When no dedicated + validator exists, report the gap and use the closest check. -## Retained Artifacts +## Token And Drift Control -- Keep transient planning, brainstorming, and Superpowers-generated working files out of `docs/`. -- `tmp/superpowers/` and `LESSONS_LEARNED.md` may hold retained work, but they do not replace canonical policy owners. -- Treat retained plans and retained learning as non-canonical until codified in the smallest valid owner. -- Keep file shape, execution workflow, and ledger row rules in retained-plan skills or owned files, not here. +- `AGENTS.md` is the canonical always-on policy surface; keep it near a 4,000 + estimated-token soft target measured as `ceil(UTF-8 bytes / 4)` by the + validator. +- Keep duplication deliberate. Duplicate only rules that must remain visible in a + specific surface and are compact enough to validate. +- When generic AGENTS.md guidance conflicts with this repository's skill-first + architecture, keep the generic idea only if it fits the ownership boundary. -## Token And Drift Control +## graphify + +This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. + +When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else. -- `AGENTS.md` is the canonical always-on policy surface; keep it near a 4,000 estimated-token soft target measured as `ceil(UTF-8 bytes / 4)` by the validator. -- Keep `.github/copilot-instructions.md` compact as a bridge and keep global review behavior in `.github/instructions/copilot-code-review.instructions.md`. -- Keep duplication deliberate. Duplicate only rules that must remain visible in a specific surface and are compact enough to validate. +- Rules: +- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. +- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. +- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. +- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. +- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). `` @@ -95,34 +132,33 @@ as consumer-repository defaults without an explicit sync contract change. ## Standards Repository Role -- This repository owns the shared Copilot customization baseline, governance contracts, catalog automation, and source-side sync tooling. -- Source-managed AI assets live mainly under `.github/`; local knowledge documents live in `docs/README.md`, `docs/repository-context.md`, `docs/architecture.md`, `docs/tech.md`, and `docs/structure.md`. -- Source-side sync command centers and sync support skills own propagation behavior. Keep their procedures in the owning sync assets, not in this bridge. -- Keep consumer-facing defaults target-agnostic. Do not encode this repository's local paths, validators, or workflow checkpoints into shared guidance unless the sync contract deliberately makes them shared. +- This repository owns the shared Copilot customization baseline, governance + contracts, catalog automation, and source-side sync tooling. +- Source-managed AI assets live mainly under `.github/`; local knowledge + documents live in `docs/README.md`, `docs/repository-context.md`, + `docs/architecture.md`, `docs/tech.md`, and `docs/structure.md`. +- Source-side sync command centers and sync support skills own propagation behavior. Keep their procedures in the owning sync assets, not in root policy. +- Keep consumer-facing defaults target-agnostic. Do not encode this repository's + local paths, validators, or workflow checkpoints into shared guidance unless + the sync contract deliberately makes them shared. ## Standards Repository Validation -- Run `make token-risks` or `python3 ./.github/scripts/detect_token_risks.py --root .` after changes that affect always-on guidance or major AI assets in this repository. -- Run the closest source-side validator for changed governance contracts, catalog families, sync behavior, or AI runtime assets. -- Address actionable source-side findings before declaring this repository's AI configuration complete. +- Run `make token-risks` or + `python3 ./.github/scripts/detect_token_risks.py --root .` after changes that + affect always-on guidance or major AI assets in this repository. +- Run the closest source-side validator for changed governance contracts, catalog + families, sync behavior, or AI runtime assets. +- Address actionable source-side findings before declaring this repository's AI + configuration complete. ## Standards Repository Locality -- Repo-local retained plans, brainstorming artifacts, and temporary analysis stay under `tmp/` or the owned retained-artifact locations named by skills. -- Consumer repositories may receive local scaffolds or override layers, but those target-local files remain owned by the consumer after materialization. -- Treat this block as source-local policy until sync automation or the sync contract gains explicit support for excluding or transforming it. +- Repo-local planning, brainstorming, temporary analysis, and working artifacts + stay outside `docs/` unless a narrower owner explicitly says otherwise. +- Consumer repositories may receive local scaffolds or override layers, but those + target-local files remain owned by the consumer after materialization. +- Treat this block as source-local policy until sync automation or the sync + contract gains explicit support for excluding or transforming it. `` - -## graphify - -This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. - -When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else. - -- Rules: -- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. -- Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. -- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. -- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. -- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb6a1b0e..41db246d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,9 +6,8 @@ This repository is the source baseline for reusable GitHub Copilot customization ## Skill-first architecture -- `AGENTS.md` is the strategic entrypoint, precedence anchor, and cross-surface bridge. -- `.github/copilot-instructions.md` is the compact repo-wide Copilot routing bridge. -- `.github/instructions/copilot-code-review.instructions.md` is the global Copilot review baseline. +- `AGENTS.md` is the strategic entrypoint, precedence anchor, and runtime agent policy surface. +- `.github/copilot-instructions.md` is review-only for GitHub.com Copilot code review. - `.github/skills/` owns reusable technical baselines, workflow depth, and specialist procedures. - `.github/agents/` owns wrapper selection, tool scope, and user-visible handoff UX. - Prompts, validators, docs, and owned files carry their own local contracts when they are the smallest valid owner. diff --git a/INTERNAL_CONTRACT.md b/INTERNAL_CONTRACT.md index 81f3fcb5..4046f1da 100644 --- a/INTERNAL_CONTRACT.md +++ b/INTERNAL_CONTRACT.md @@ -6,7 +6,7 @@ Treat the current skill-first architecture as the source of truth. Do not infer ## Principles - Validate rules, not file size or historical implementation details. -- Treat rules as canonical and files as bridge or owner-specific surfaces. +- Treat rules as canonical and files as entrypoint or owner-specific surfaces. - Keep stable policy separate from volatile inventory. - Let the smallest valid owner hold each rule. - Keep reusable technical guidance in skills, with deeper detail in skill references only when it is still valuable. @@ -18,18 +18,30 @@ Treat the current skill-first architecture as the source of truth. Do not infer #### `skill-first-root-agents-is-entrypoint` -- Goal: keep `AGENTS.md` as the stable repository entrypoint, orientation document, and cross-surface strategic operating bridge. +- Goal: keep `AGENTS.md` as the stable repository entrypoint, orientation document, and cross-surface strategic policy surface. - Scope: - root `AGENTS.md` - - `.github/copilot-instructions.md` - `.github/INVENTORY.md` - Expected behavior: - root `AGENTS.md` defines the strategic role of the AI configuration system, precedence model, tactical operating defaults, and default language rule - root `AGENTS.md` points to `.github/INVENTORY.md` as the exact live catalog - - root `AGENTS.md` defines rule placement early so operational procedures do not drift into the always-on bridge + - root `AGENTS.md` defines rule placement early so operational procedures do not drift into the always-on entrypoint - root `AGENTS.md` may carry compact tactical defaults for mode selection, owner visibility, validation evidence, and root-cause preference - root `AGENTS.md` does not carry volatile inventory, file-shape recipes, command playbooks, domain checklists, or long surface-specific procedures +#### `skill-first-root-agents-projects-current-baseline-shape` + +- Goal: preserve the current root-policy shape without restoring the retired bridge or context-routing model. +- Scope: + - root `AGENTS.md` + - `INTERNAL_CONTRACT.md` + - source-side contract tests +- Expected behavior: + - root `AGENTS.md` may carry a portable `` block when the source repository intentionally projects shared policy into consumer repositories + - root `AGENTS.md` may carry a source-local `` block that is explicitly non-portable by default + - root `AGENTS.md` may keep compact graph orientation rules when those rules are deliberately part of the declared shared baseline + - tests and validators must align to the current on-disk root-policy shape instead of assuming a separate `## Context Routing` section or the absence of root-level graph guidance + #### `skill-first-inventory-is-externalized` - Goal: keep policy and live catalog inventory decoupled. @@ -41,15 +53,15 @@ Treat the current skill-first architecture as the source of truth. Do not infer - policy files may point to the inventory but do not duplicate it - generated inventory reflects current skills, agents, prompts, workflows, and other managed AI assets without becoming policy -#### `skill-first-copilot-bridge-stays-meaningful` +#### `skill-first-copilot-review-stays-bounded` -- Goal: keep `.github/copilot-instructions.md` operationally significant as a compact routing bridge for native Copilot surfaces. +- Goal: keep `.github/copilot-instructions.md` limited to the review-only behavior that still needs that file. - Scope: - `.github/copilot-instructions.md` - Expected behavior: - - the file stays compact, high-signal, and aligned with `AGENTS.md` - - the file routes to canonical owners and does not duplicate full policy blocks - - the file is not reduced to an empty shell or replaced by inventory text + - the file stays compact, high-signal, and explicitly review-scoped + - the file does not duplicate `AGENTS.md` policy blocks or skill-owned procedures + - the file is not used as a general runtime contract for coding agents #### `skill-first-domain-skills-are-canonical` @@ -59,20 +71,18 @@ Treat the current skill-first architecture as the source of truth. Do not infer - `.github/skills/internal-*/references/**` - `.github/skills/internal-*/agents/openai.yaml` - `AGENTS.md` - - `.github/copilot-instructions.md` - Expected behavior: - umbrella skills own lightweight domain baselines and route to specialist depth only when needed - specialist skills own detailed workflows, framework behavior, validation shape, and domain-specific edge cases - large checklists, templates, and examples live in skill references only when they remain useful and are not copied into always-on files - `agents/openai.yaml` metadata stays aligned with each repository-owned skill bundle when present - - Codex and OpenCode portability depends on skills and home-skill sync, not on oversized bridge content + - Codex and OpenCode portability depends on skills and home-skill sync, not on oversized always-on content #### `skill-first-routing-is-evidence-based` - Goal: keep owner selection deterministic and visible without creating a hidden router. - Scope: - `AGENTS.md` - - `.github/copilot-instructions.md` - `.github/skills/internal-gateway-idea-brainstorming/**` - `.github/skills/internal-gateway-review/**` - `.github/skills/internal-gateway-simple-task/**` @@ -91,7 +101,6 @@ Treat the current skill-first architecture as the source of truth. Do not infer - Goal: keep repository knowledge documents ordered and owned by the right source. - Scope: - `AGENTS.md` - - `.github/copilot-instructions.md` - `docs/README.md` - `docs/repository-context.md` - `docs/architecture.md` @@ -120,14 +129,12 @@ Treat the current skill-first architecture as the source of truth. Do not infer - Goal: centralize the repository language default. - Scope: - root `AGENTS.md` - - `.github/copilot-instructions.md` - skills - agents - prompts - owned files with local entry rules - Expected behavior: - root `AGENTS.md` states that the default authoring language for repository artifacts is English unless a narrower owned file, skill, or local exception explicitly overrides it - - `.github/copilot-instructions.md` may project that rule in compact form for Copilot flows - local files do not restate the rule in broader or stricter terms than the canonical default without declaring an explicit reason #### `language-exceptions-are-explicit-and-local` @@ -169,13 +176,11 @@ Treat the current skill-first architecture as the source of truth. Do not infer #### `duplication-useful-restatements-are-allowed` -- Goal: preserve local self-containment when it materially helps the consumer. +- Goal: preserve local self-containment when it materially helps the owning surface. - Scope: - - `.github/copilot-instructions.md` - governance agents and prompts - skills and skill references - Expected behavior: - - compact repo-wide bridge restatements remain allowed - local restatements remain allowed when they improve behavior for the target surface and stay aligned with the canonical rule - duplication must be deliberate and low-drift @@ -187,7 +192,7 @@ Treat the current skill-first architecture as the source of truth. Do not infer - contract files - governance assets - Expected behavior: - - removed validators, removed sync scripts, removed contract tests, and retired bridge-era assets are not described as active requirements + - removed validators, removed sync scripts, removed contract tests, and retired assets are not described as active requirements - 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 @@ -245,9 +250,8 @@ Treat the current skill-first architecture as the source of truth. Do not infer #### `resource-governance-canonical-operational-model-stays-explicit` -- Goal: keep the canonical repository-owned operating model clear across bridge and owner-specific surfaces. +- Goal: keep the canonical repository-owned operating model clear across owner-specific surfaces. - Scope: - - `.github/copilot-instructions.md` - canonical operational wrapper agents - shared operating-model skills - Expected behavior: @@ -267,12 +271,11 @@ Treat the current skill-first architecture as the source of truth. Do not infer #### `repository-workflow-github-pr-merge-and-terminal-state-reminders-stay-owned` -- Goal: keep repo-wide GitHub PR operating reminders visible without expanding the Copilot bridge. +- Goal: keep repo-wide GitHub PR operating reminders visible without expanding always-on guidance. - Scope: - `.github/skills/internal-github-pr/SKILL.md` - `CODEOWNERS` - `.github/skills/internal-github-governance/SKILL.md` - - `.github/copilot-instructions.md` - Expected behavior: - self-authored PRs under required reviews are not treated as mergeable from green checks alone - the GitHub PR skill tells operators to verify a qualifying non-author approval before merge @@ -280,15 +283,14 @@ Treat the current skill-first architecture as the source of truth. Do not infer - organization-wide `gh search prs` results are treated as potentially stale immediately after merge - repository-scoped `gh pr view --json state,mergedAt` is used to confirm terminal PR state before treating a just-merged PR as still open - `CODEOWNERS` placeholder-owner rules stay in the owned file and GitHub governance owner instead of always-on guidance - - the Copilot bridge does not repeat the full workflow reminder text + - always-on guidance does not repeat the full workflow reminder text ### Reporting -#### `reporting-completion-report-bridge-stays-visible` +#### `reporting-completion-report-stays-owned` - Goal: keep the completion report contract visible on the surfaces that need it while keeping Copilot always-on guidance short. - Scope: - - `.github/copilot-instructions.md` - `.github/README.md` - relevant governance or sync agents - Expected behavior: @@ -296,7 +298,6 @@ Treat the current skill-first architecture as the source of truth. Do not infer - maintainer-facing docs and sync contracts may define detailed report labels such as `✅ Outcome`, `🤖 Agents`, `📘 Instructions`, `🧩 Skills`, and `📦 Other Resources` - supporting sections such as `🤖 Agents`, `📘 Instructions`, `🧩 Skills`, and `📦 Other Resources` are optional detail by default - when detail is available but omitted for token discipline, the response offers a compact follow-up and accepts number-only replies - - the Copilot bridge keeps only a compact reporting reminder unless a narrower contract requires more detail - root `AGENTS.md` does not need to carry the full formatting contract #### `reporting-retained-learning-ledger-stays-governed` @@ -304,17 +305,15 @@ Treat the current skill-first architecture as the source of truth. Do not infer - Goal: preserve retained learning without turning it into shadow policy. - Scope: - root `AGENTS.md` - - `.github/copilot-instructions.md` - `LESSONS_LEARNED.md` - `.github/skills/internal-lesson-codification/SKILL.md` - Expected behavior: - repository-root `LESSONS_LEARNED.md` exists as a non-canonical learning ledger - completed tasks add only durable, reusable lessons that were not already codified when discovered - durable corrections to repeated or consequential misapplication of existing repository rules may also be retained as lessons - - root `AGENTS.md` keeps only retained-artifact boundaries and points detailed procedures to retained-plan and lesson-codification owners + - root `AGENTS.md` stays free of retained-learning paths, retained-plan paths, and ledger mechanics; ownership is covered by the general smallest-owner policy - the lesson codification skill owns the workflow that routes candidate lessons to the smallest valid canonical owner before ledger fallback - detailed ledger row preservation rules live in `LESSONS_LEARNED.md` entry rules - - the Copilot bridge keeps only the retained-artifact principle and owner-routing reminder - no ledger update is required when no stable new lesson emerged - once a lesson is codified elsewhere, it is removed from `LESSONS_LEARNED.md` instead of being retained as a codified duplicate @@ -327,7 +326,6 @@ Treat the current skill-first architecture as the source of truth. Do not infer - future validator, sync, and test implementations - Expected behavior: - automation reads `AGENTS.md` for cross-surface defaults and precedence - - automation reads `.github/copilot-instructions.md` for repo-wide Copilot bridge behavior - automation reads `.github/INVENTORY.md` for the live catalog - automation reads skill bundles for domain baselines, reusable procedures, references, and metadata - automation treats routing helpers as advisory evidence, not hidden dispatch authority diff --git a/Makefile b/Makefile index b2d6b45c..8a64bdeb 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ SHELL_SCRIPTS := $(wildcard .github/scripts/*.sh) PYTHON_PATHS := .github/scripts/*.py .github/scripts/lib tests .github/skills SCRIPTS_RUNNER := ./.github/scripts/run.sh SCRIPTS_VENV := .github/scripts/.venv +RUFF := $(if $(wildcard $(SCRIPTS_VENV)/bin/ruff),$(SCRIPTS_VENV)/bin/ruff,ruff) CATALOG_FAST_TESTS := tests/test_inventory_and_consistency.py tests/test_validation_entrypoints_contract.py tests/test_retained_plan_artifact_contract.py tests/github/scripts/test_cli_entrypoints.py CATALOG_FAST_INCLUDE_TOKEN_RISKS ?= 0 MARKDOWNLINT_VERSION := 0.18.1 @@ -27,10 +28,12 @@ lint: python-version-check docs-lint @if [ -n "$(SHELL_SCRIPTS)" ]; then bash -n $(SHELL_SCRIPTS); else printf '%s\n' 'No Bash scripts to lint.'; fi @if command -v shellcheck >/dev/null 2>&1; then shellcheck -s bash $(SHELL_SCRIPTS); else printf '%s\n' 'shellcheck not installed; skipping.'; fi $(PYTHON) -m compileall -q $(PYTHON_PATHS) + $(RUFF) check .github/scripts tools tests catalog-lint: python-version-check @if [ -n "$(SHELL_SCRIPTS)" ]; then bash -n $(SHELL_SCRIPTS); else printf '%s\n' 'No Bash scripts to lint.'; fi $(PYTHON) -m compileall -q $(PYTHON_PATHS) + $(RUFF) check .github/scripts tools tests catalog-fast-check: scripts-bootstrap @$(SCRIPTS_RUNNER) build_inventory --root . --check diff --git a/docs/architecture.md b/docs/architecture.md index 1394c8e8..54aec8cf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,7 +10,7 @@ by repository evidence. In scope: -- Source policy bridge and Copilot projection. +- Source agent-policy entrypoint and review-only Copilot configuration. - Repository-owned skills, agents, prompts, and templates. - Catalog build and consistency tooling. - Sync planning and apply automation. @@ -32,9 +32,9 @@ Out of scope: | Component | Path | Responsibility | | --- | --- | --- | -| Strategic bridge | `AGENTS.md` | Stable precedence, ownership, and tactical defaults. | -| Copilot bridge | `.github/copilot-instructions.md` | Compact repo-wide Copilot routing bridge to canonical policy. | -| Global review baseline | `.github/instructions/copilot-code-review.instructions.md` | Source-managed defect-first review behavior for all paths. | +| Agent policy entrypoint | `AGENTS.md` | Stable precedence, ownership, and tactical defaults. | +| Copilot review file | `.github/copilot-instructions.md` | Review-only behavior for GitHub.com Copilot code review. | +| Review assets | `.github/` review-scoped assets | Source-managed defect-first review behavior kept outside runtime agent policy. | | Knowledge docs | `docs/` | Descriptive repository context, architecture, tech, and structure. | | Skills and agents | `.github/skills/`, `.github/agents/` | Reusable guidance plus route wrappers and command centers. | | Prompts and templates | `.github/prompts/`, `.github/templates/` | Targeted creators and consumer scaffold sources. | diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..2193aba4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.ruff] +line-length = 88 +target-version = "py313" +exclude = [ + ".github/scripts/.venv", + "tools/analyze_copilot_debug_log/.venv", +] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "I"] diff --git a/tests/github/scripts/lib/test_jsonc.py b/tests/github/scripts/lib/test_jsonc.py new file mode 100644 index 00000000..aa8d8d82 --- /dev/null +++ b/tests/github/scripts/lib/test_jsonc.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import pytest +from lib.jsonc import ( + JsoncParseError, + apply_managed_vscode_copilot_settings, + consume_jsonc_string, + parse_jsonc_root_object, +) + + +def test_parse_jsonc_root_object_handles_comments_and_nested_members() -> None: + text = ( + "{\n" + " // Root comment\n" + ' "github.copilot.chat.codeGeneration.useInstructionFiles": true,\n' + ' "chat.instructionsFilesLocations": {\n' + " /* nested comment */\n" + ' ".github/instructions": true\n' + " }\n" + "}\n" + ) + + root = parse_jsonc_root_object(text) + + assert [member.key for member in root.members] == [ + "github.copilot.chat.codeGeneration.useInstructionFiles", + "chat.instructionsFilesLocations", + ] + assert root.members[1].object_value is not None + assert [member.key for member in root.members[1].object_value.members] == [ + ".github/instructions" + ] + + +def test_consume_jsonc_string_decodes_escape_sequences() -> None: + value, end = consume_jsonc_string('"line\\n\\u0041\\/end"', 0) + + assert value == "line\nA/end" + assert end == len('"line\\n\\u0041\\/end"') + + +def test_apply_managed_vscode_copilot_settings_merges_boolean_values() -> None: + original = ( + "{\n" + ' "github.copilot.chat.codeGeneration.useInstructionFiles": true,\n' + ' "chat.instructionsFilesLocations": {}\n' + "}\n" + ) + + updated = apply_managed_vscode_copilot_settings(original) + + assert '"github.copilot.chat.codeGeneration.useInstructionFiles": false' in updated + assert '".github/instructions": false' in updated + + +def test_apply_managed_vscode_copilot_settings_rejects_duplicate_keys() -> None: + duplicate = ( + "{\n" + ' "github.copilot.chat.codeGeneration.useInstructionFiles": true,\n' + ' "github.copilot.chat.codeGeneration.useInstructionFiles": false\n' + "}\n" + ) + + with pytest.raises(JsoncParseError, match="duplicate key"): + apply_managed_vscode_copilot_settings(duplicate) diff --git a/tests/github/scripts/test_cli_entrypoints.py b/tests/github/scripts/test_cli_entrypoints.py index ec2cae9c..4bc207e4 100644 --- a/tests/github/scripts/test_cli_entrypoints.py +++ b/tests/github/scripts/test_cli_entrypoints.py @@ -71,7 +71,7 @@ def write_valid_internal_skill(skill_dir: Path, skill_name: str) -> None: f"# {skill_name}\n\n" "## When to use\n" "- Use when validating repository-owned skill metadata safely.\n\n" - "Use `AGENTS.md` for bridge context.\n", + "Use `AGENTS.md` for root policy context.\n", ) write_file( skill_dir / "agents/openai.yaml", @@ -249,7 +249,7 @@ def test_detect_token_risks_main_respects_strict_mode( [ "- Keep policy separate from inventory.", "- Keep AGENTS.md strategic and stable.", - "- Keep .github/copilot-instructions.md as the projection layer.", + "- Keep .github/copilot-instructions.md review-only.", "- Keep .github/INVENTORY.md as the exact catalog.", "- Preserve explicit precedence rules.", "- Remove overlap instead of keeping compatibility copies.", @@ -273,7 +273,7 @@ def test_detect_token_risks_main_respects_strict_mode( finding_codes = {item["code"] for item in payload} assert exit_code == 1 - assert "bridge-overlap" in finding_codes + assert "root-policy-overlap" in finding_codes def test_audit_copilot_catalog_main_groups_blocking_findings( diff --git a/tests/github/scripts/test_repo_owned_script_coverage.py b/tests/github/scripts/test_repo_owned_script_coverage.py index 9e377a19..6c0522e4 100644 --- a/tests/github/scripts/test_repo_owned_script_coverage.py +++ b/tests/github/scripts/test_repo_owned_script_coverage.py @@ -9,6 +9,7 @@ ".github/scripts/build_inventory.py", ".github/scripts/check_catalog_consistency.py", ".github/scripts/detect_token_risks.py", + ".github/scripts/lib/cli_runner.py", ".github/scripts/sync_copilot_catalog.py", ".github/scripts/validate_critical_output.py", ".github/scripts/validate_internal_skills.py", @@ -37,6 +38,9 @@ ".github/scripts/lib/syncing.py", ".github/scripts/lib/token_risks.py", }, + "tests/github/scripts/lib/test_jsonc.py": { + ".github/scripts/lib/jsonc.py", + }, "tests/github/scripts/test_benchmark_skill_tokens.py": { ".github/scripts/benchmark_skill_tokens.py", }, diff --git a/tests/github/skills/internal_gateway_idea_brainstorming/scripts/test_audit_contract.py b/tests/github/skills/internal_gateway_idea_brainstorming/scripts/test_audit_contract.py index 97b67ba8..440fe7fc 100644 --- a/tests/github/skills/internal_gateway_idea_brainstorming/scripts/test_audit_contract.py +++ b/tests/github/skills/internal_gateway_idea_brainstorming/scripts/test_audit_contract.py @@ -33,7 +33,7 @@ def test_audit_reports_minimal_marker_shape() -> None: "plan_approval_gate", "handoff_gate_4", "ask_before_critical", - "ownership_after_critical", + "direct_vs_plan_recommendation", "explicit_plan_approval", "alias_mapping", } diff --git a/tests/github/skills/internal_gateway_simple_task/scripts/test_resolve_simple_task.py b/tests/github/skills/internal_gateway_simple_task/scripts/test_resolve_simple_task.py index 31d46159..2e29faf3 100644 --- a/tests/github/skills/internal_gateway_simple_task/scripts/test_resolve_simple_task.py +++ b/tests/github/skills/internal_gateway_simple_task/scripts/test_resolve_simple_task.py @@ -100,6 +100,10 @@ def test_claim_resolution_for_fixed_requires_debugging_and_verification() -> Non "owner": "internal-debugging", "evidence_gate": "Re-run the original loop, or state the blocker.", }, + { + "owner": "internal-gateway-simple-task", + "evidence_gate": "Run Direct Completion Control over every in-scope source item and mandatory applicable requirement before the claim.", + }, { "owner": "superpowers-verification-before-completion", "evidence_gate": "Fresh validation evidence, not intent or stale output.", @@ -116,6 +120,10 @@ def test_claim_resolution_deduplicates_verification_owner() -> None: "owner": "internal-github-pr", "evidence_gate": "Check PR lifecycle evidence before the claim.", }, + { + "owner": "internal-gateway-simple-task", + "evidence_gate": "Run Direct Completion Control over every in-scope source item and mandatory applicable requirement before the claim.", + }, { "owner": "superpowers-verification-before-completion", "evidence_gate": "Fresh validation evidence, not intent or stale output.", @@ -123,6 +131,19 @@ def test_claim_resolution_deduplicates_verification_owner() -> None: ] +def test_direct_execution_claims_require_completion_control() -> None: + module = load_script_module() + requirements = module.resolve_claim_requirements( + ["completion", "readiness", "validator-passes"] + ) + + assert requirements[0] == { + "owner": "internal-gateway-simple-task", + "evidence_gate": "Run Direct Completion Control over every in-scope source item and mandatory applicable requirement before the claim.", + } + assert requirements[1]["owner"] == "superpowers-verification-before-completion" + + def lanes_from_simple_lanes_reference(reference_text: str) -> set[str]: table_start = reference_text.index("## Lane Selection") table_end = reference_text.index("\n## ", table_start + 1) diff --git a/tests/github/skills/internal_gateway_writing_plans/scripts/test_plan_authoring.py b/tests/github/skills/internal_gateway_writing_plans/scripts/test_plan_authoring.py index ffee4cea..2caaef54 100644 --- a/tests/github/skills/internal_gateway_writing_plans/scripts/test_plan_authoring.py +++ b/tests/github/skills/internal_gateway_writing_plans/scripts/test_plan_authoring.py @@ -231,6 +231,89 @@ def test_handoff_check_warns_on_oversized_compact_summary(tmp_path: Path) -> Non ) +def test_audit_warns_when_summary_hides_counter_validation_facts( + tmp_path: Path, +) -> None: + plan_folder = ( + tmp_path / "tmp" / "superpowers" / "mini-plan-counter-validation-warning" + ) + _write_compact_plan(plan_folder) + (plan_folder / "01-change-summary.md").write_text( + "## Problema da risolvere\n" + "Allineare il piano a un nuovo contratto dati per output generati.\n" + "## Risultato atteso\n" + "- Gli output rigenerati rispettano il nuovo schema e correggono i dati.\n" + "## Risorse coinvolte\n" + "| Risorsa | Azione | Scopo |\n" + "| --- | --- | --- |\n" + "| Aggregate + slide outputs | update | Allineare gli output |\n" + "## Decisione richiesta\n" + "Approvare il piano.\n" + "## Decisioni aperte\n" + "none\n", + encoding="utf-8", + ) + execution_path = plan_folder / "02-execution.md" + execution_path.write_text( + execution_path.read_text(encoding="utf-8").replace( + "| PLAN-01 | Empty-section detection | `handoff-check` rejects empty sections | repository | failing then passing test | PENDING | `02-execution.md` |", + "| PLAN-01 | Aggregate and slide data contract | `impacted_platforms` and `impacted_subscriptions` become the first aggregate columns; `technology_or_service` stays before `retiring_feature`; `source_links` never stays empty and missing values trigger a blocking diagnostic; rows 27-33 are restored with no row-count loss | repository | audit plus focused file diff | PENDING | `02-execution.md` |", + ), + encoding="utf-8", + ) + + result = run_cli("audit", plan_folder, "--format", "json") + payload = json.loads(result.stdout) + + assert result.returncode == 0 + assert payload["ready"] is True + assert any( + f["code"] == "summary-missing-counter-validation-facts" + and f["severity"] == "WARNING" + for f in payload["findings"] + ) + + +def test_audit_accepts_summary_with_counter_validation_facts(tmp_path: Path) -> None: + plan_folder = tmp_path / "tmp" / "superpowers" / "mini-plan-counter-validation-ok" + _write_compact_plan(plan_folder) + (plan_folder / "01-change-summary.md").write_text( + "## Problema da risolvere\n" + "Allineare il piano a un nuovo contratto dati per output generati.\n" + "## Risultato atteso\n" + "- Aggregate TSV con `impacted_platforms` e `impacted_subscriptions` come prime colonne.\n" + "- `technology_or_service` resta prima di `retiring_feature`; `source_links` e sempre valorizzato oppure scatta una diagnostica bloccante.\n" + "- Le righe 27-33 vengono ripristinate senza perdita di righe.\n" + "## Risorse coinvolte\n" + "| Risorsa | Azione | Scopo |\n" + "| --- | --- | --- |\n" + "| Aggregate + slide outputs | update | Preservare ordine colonne, campi obbligatori e integrita dati |\n" + "## Decisione richiesta\n" + "Approvare il piano.\n" + "## Decisioni aperte\n" + "none\n", + encoding="utf-8", + ) + execution_path = plan_folder / "02-execution.md" + execution_path.write_text( + execution_path.read_text(encoding="utf-8").replace( + "| PLAN-01 | Empty-section detection | `handoff-check` rejects empty sections | repository | failing then passing test | PENDING | `02-execution.md` |", + "| PLAN-01 | Aggregate and slide data contract | `impacted_platforms` and `impacted_subscriptions` become the first aggregate columns; `technology_or_service` stays before `retiring_feature`; `source_links` never stays empty and missing values trigger a blocking diagnostic; rows 27-33 are restored with no row-count loss | repository | audit plus focused file diff | PENDING | `02-execution.md` |", + ), + encoding="utf-8", + ) + + result = run_cli("audit", plan_folder, "--format", "json") + payload = json.loads(result.stdout) + + assert result.returncode == 0 + assert payload["ready"] is True + assert not any( + f["code"] == "summary-missing-counter-validation-facts" + for f in payload["findings"] + ) + + def test_handoff_check_rejects_extended_control_without_ordered_validation( tmp_path: Path, ) -> None: diff --git a/tests/github/skills/local_agent_sync_install_ai_resources/scripts/test_bisync_skills.py b/tests/github/skills/local_agent_sync_install_ai_resources/scripts/test_bisync_skills.py index 9596ae25..770d7d74 100644 --- a/tests/github/skills/local_agent_sync_install_ai_resources/scripts/test_bisync_skills.py +++ b/tests/github/skills/local_agent_sync_install_ai_resources/scripts/test_bisync_skills.py @@ -1,5 +1,6 @@ from __future__ import annotations +import argparse import importlib.util import json import os @@ -192,6 +193,28 @@ def test_build_plan_detects_home_to_repo_direction(tmp_path: Path) -> None: assert drift.direction == "home-to-repo" +def test_build_plan_treats_hash_equal_different_mtime_as_in_sync( + tmp_path: Path, +) -> None: + source = tmp_path / "source" + home = tmp_path / "home" + init_git_repo(source) + source_skill = make_skill( + source / ".github" / "skills", "same-hash-skill", "# Same content\n" + ) + home_skill = make_skill( + home / ".agents" / "skills", "same-hash-skill", "# Same content\n" + ) + set_tree_mtime(source_skill, 200.0) + set_tree_mtime(home_skill, 100.0) + + plan = build_bisync_plan(source, home, mode="plan") + + assert plan.drifts == [] + assert plan.blocked_codes == [] + assert plan.verification["status"] == "ok" + + def test_build_plan_blocks_equal_mtime(tmp_path: Path) -> None: source = tmp_path / "source" home = tmp_path / "home" @@ -228,7 +251,7 @@ def test_build_plan_blocks_only_repo_and_only_home(tmp_path: Path) -> None: assert plan.blocked_codes == ["bisync-only-home", "bisync-only-repo"] -def test_excludes_local_agent_sync_bundles_from_scan(tmp_path: Path) -> None: +def test_excludes_local_prefixed_bundles_from_scan(tmp_path: Path) -> None: source = tmp_path / "source" home = tmp_path / "home" init_git_repo(source) @@ -242,6 +265,8 @@ def test_excludes_local_agent_sync_bundles_from_scan(tmp_path: Path) -> None: "local-agent-sync-install-ai-resources", "# Diverged sync bundle\n", ) + make_skill(source / ".github" / "skills", "local-custom", "# Local source\n") + make_skill(home / ".agents" / "skills", "local-custom", "# Local home\n") make_skill(source / ".github" / "skills", "normal-skill", "# Normal\n") make_skill(home / ".agents" / "skills", "normal-skill", "# Normal\n") @@ -270,6 +295,50 @@ def test_apply_blocks_dirty_repository_without_writes(tmp_path: Path) -> None: assert (home_skill / "SKILL.md").read_text(encoding="utf-8") == "# Home\n" +def test_run_bisync_apply_allows_only_repo_blocker(tmp_path: Path) -> None: + source = tmp_path / "source" + home = tmp_path / "home" + init_git_repo(source) + make_skill(source / ".github" / "skills", "repo-only", "# Repo only\n") + (home / ".agents" / "skills").mkdir(parents=True, exist_ok=True) + commit_all(source, "add repo-only") + + args = argparse.Namespace( + source_root=source.as_posix(), + home_root=home.as_posix(), + format="json", + ) + + exit_code = bisync_skills.run_bisync_apply(args) + + assert exit_code == 0 + assert (home / ".agents" / "skills" / "repo-only" / "SKILL.md").read_text( + encoding="utf-8" + ) == "# Repo only\n" + verify_plan = build_bisync_plan(source, home, mode="plan") + assert verify_plan.drifts == [] + + +def test_run_bisync_apply_blocks_non_resolvable_codes(tmp_path: Path) -> None: + source = tmp_path / "source" + home = tmp_path / "home" + init_git_repo(source) + make_skill(source / ".github" / "skills", "repo-only", "# Repo only\n") + make_skill(home / ".agents" / "skills", "home-only", "# Home only\n") + commit_all(source, "add repo-only") + + args = argparse.Namespace( + source_root=source.as_posix(), + home_root=home.as_posix(), + format="json", + ) + + exit_code = bisync_skills.run_bisync_apply(args) + + assert exit_code == 1 + assert not (home / ".agents" / "skills" / "repo-only").exists() + + def test_apply_repo_to_home_converges_without_manifest_entry( tmp_path: Path, ) -> None: @@ -486,7 +555,7 @@ def test_plan_json_output_contains_structured_next_action(tmp_path: Path) -> Non assert payload["next_action"]["requires_explicit_approval"] is True -def test_emit_text_output_groups_repo_home_buckets( +def test_emit_report_output_groups_repo_home_buckets( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: source = tmp_path / "source" @@ -502,15 +571,14 @@ def test_emit_text_output_groups_repo_home_buckets( set_tree_mtime(source_skill, 200.0) plan = build_bisync_plan(source, home, mode="plan") - bisync_skills._emit_bisync_output(plan, "text") + bisync_skills._emit_bisync_output(plan, "report") output = capsys.readouterr().out assert "repo-only" in output assert "home-only" in output assert "repo-to-home" in output - assert "winner: repo" in output - assert "loser: home" in output - assert "blocker: bisync-only-repo" in output + assert "Repository bundle timestamp is newer than home bundle." in output + assert "bisync-only-repo" in output def test_source_root_missing_skills_dir_returns_blocker(tmp_path: Path) -> None: diff --git a/tests/github/skills/local_agent_sync_install_ai_resources/scripts/test_home_syncing.py b/tests/github/skills/local_agent_sync_install_ai_resources/scripts/test_home_syncing.py index a3d42a8c..0b117e47 100644 --- a/tests/github/skills/local_agent_sync_install_ai_resources/scripts/test_home_syncing.py +++ b/tests/github/skills/local_agent_sync_install_ai_resources/scripts/test_home_syncing.py @@ -530,7 +530,7 @@ def test_repo_to_home_bisync_refreshes_install_manifest(tmp_path: Path) -> None: assert "target-modified-managed" not in blocked_codes -def test_direct_home_edit_still_blocks_target_modified_managed( +def test_home_newer_managed_drift_becomes_warning_until_bisync_review( tmp_path: Path, ) -> None: source_root = tmp_path / "source" @@ -547,8 +547,55 @@ def test_direct_home_edit_still_blocks_target_modified_managed( ) apply_home_sync_plan(install_plan, create_missing_dirs=True) - copied_skill = home_root / ".agents/skills/demo-skill/SKILL.md" - copied_skill.write_text("# locally edited\n", encoding="utf-8") + source_skill = source_root / ".github/skills/demo-skill" + copied_skill = home_root / ".agents/skills/demo-skill" + write_file(copied_skill / "SKILL.md", "# locally edited\n") + set_tree_mtime(source_skill, 100.0) + set_tree_mtime(copied_skill, 200.0) + + review_plan = build_home_sync_plan( + source_root=source_root, + home_root=home_root, + targets=parse_targets("skills"), + mode="apply", + ) + blocked_codes = { + operation.code + for operation in review_plan.operations + if operation.action == "blocked" and operation.code + } + warning_codes = { + operation.code + for operation in review_plan.operations + if operation.action == "warning" and operation.code + } + + assert "target-modified-managed" not in blocked_codes + assert "target-modified-managed" in warning_codes + + +def test_ambiguous_managed_drift_still_blocks_target_modified_managed( + tmp_path: Path, +) -> None: + source_root = tmp_path / "source" + home_root = tmp_path / "home" + initialize_source_repo(source_root) + init_git_repo(source_root) + commit_all(source_root, "initial source bundle") + + install_plan = build_home_sync_plan( + source_root=source_root, + home_root=home_root, + targets=parse_targets("skills"), + mode="apply", + ) + apply_home_sync_plan(install_plan, create_missing_dirs=True) + + source_skill = source_root / ".github/skills/demo-skill" + copied_skill = home_root / ".agents/skills/demo-skill" + write_file(copied_skill / "SKILL.md", "# locally edited\n") + set_tree_mtime(source_skill, 100.0) + set_tree_mtime(copied_skill, 100.0) blocked_plan = build_home_sync_plan( source_root=source_root, @@ -794,20 +841,12 @@ def test_apply_can_retire_selected_targets_without_touching_remaining_targets( def test_home_sync_catalog_does_not_contain_internal_graphify_in_real_repo() -> None: - catalog_path = ( - REPO_ROOT - / ".github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml" - ) - catalog = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) - resource_ids = {r["resource_id"] for r in catalog.get("resources", [])} + catalog = home_syncing.load_home_sync_catalog(REPO_ROOT) + resource_ids = {resource.resource_id for resource in catalog} assert "internal-graphify" not in resource_ids def test_home_sync_catalog_contains_internal_ai_resource_review_in_real_repo() -> None: - catalog_path = ( - REPO_ROOT - / ".github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml" - ) - catalog = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) - resource_ids = {r["resource_id"] for r in catalog.get("resources", [])} + catalog = home_syncing.load_home_sync_catalog(REPO_ROOT) + resource_ids = {resource.resource_id for resource in catalog} assert "internal-ai-resource-review" in resource_ids diff --git a/tests/test_completion_report_contract.py b/tests/test_completion_report_contract.py index e317ed82..03214d2b 100644 --- a/tests/test_completion_report_contract.py +++ b/tests/test_completion_report_contract.py @@ -10,10 +10,15 @@ def read_text(relative_path: str) -> str: def test_repo_projection_keeps_completion_report_compact() -> None: copilot_text = read_text(".github/copilot-instructions.md") - assert "Report completed work with outcome, changed files" in copilot_text - assert "validation results" in copilot_text - assert "remaining gaps" in copilot_text - assert "Include detailed resource sections only when the user asks" in copilot_text + assert "This file is only for GitHub.com Copilot code review." in copilot_text + assert "It is not a general task-execution guide" in copilot_text + assert "Review changed files for defects that matter before merge." in copilot_text + assert "Report findings first, ordered by severity." in copilot_text + assert ( + "Do not treat this file as instructions for coding agents, local CLIs, or" + in copilot_text + ) + assert "Report completed work with outcome, changed files" not in copilot_text assert "`✅ Outcome`" not in copilot_text assert "`1 = resources used`" not in copilot_text diff --git a/tests/test_home_ai_resources_sync.py b/tests/test_home_ai_resources_sync.py index e2e532c6..d0abc10c 100644 --- a/tests/test_home_ai_resources_sync.py +++ b/tests/test_home_ai_resources_sync.py @@ -2,17 +2,28 @@ import argparse import json +import os import subprocess from pathlib import Path import sync_home_ai_resources +def sync_cli_module(): + return getattr(sync_home_ai_resources, "SKILL_CLI", sync_home_ai_resources) + + def write_file(path: Path, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") +def set_tree_mtime(root: Path, timestamp: float) -> None: + for path in sorted(root.rglob("*")): + os.utime(path, (timestamp, timestamp)) + os.utime(root, (timestamp, timestamp)) + + def initialize_source_repo(root: Path) -> None: write_file(root / "AGENTS.md", "# AGENTS\n") write_file( @@ -492,7 +503,7 @@ def test_sync_stops_for_bisync_drift_after_clean_install( home_root=str(home_root), targets="skills", retire_targets="", - create_missing_dirs=False, + create_missing_dirs=True, prune_managed=False, experimental_targets=False, format="json", @@ -511,6 +522,110 @@ def test_sync_stops_for_bisync_drift_after_clean_install( assert payload["bisync"]["drifts"][0]["skill"] == "home-only" +def test_sync_allows_safe_repo_only_bisync_after_clean_install( + monkeypatch, tmp_path: Path, capsys +) -> None: + source_root = tmp_path / "source" + home_root = tmp_path / "home" + initialize_source_repo(source_root) + write_file(source_root / ".github/skills/repo-only-extra/SKILL.md", "# Repo only\n") + monkeypatch.setattr( + sync_home_ai_resources, + "parse_args", + lambda _=None: argparse.Namespace( + command="sync", + source_root=str(source_root), + home_root=str(home_root), + targets="skills", + retire_targets="", + create_missing_dirs=True, + prune_managed=False, + experimental_targets=False, + format="json", + fast=False, + changed_only=False, + ), + ) + + exit_code = sync_home_ai_resources.main() + payload = json.loads(capsys.readouterr().out) + + assert exit_code == 0 + assert payload["mode"] == "sync" + assert payload["status"] == "done" + assert payload["bisync"]["blocked_codes"] == ["bisync-only-repo"] + assert payload["bisync"]["drifts"] == [ + { + "direction": "repo-to-home", + "home": str(home_root / ".agents/skills/repo-only-extra"), + "repo": str(source_root / ".github/skills/repo-only-extra"), + "skill": "repo-only-extra", + "type": "only-repo", + } + ] + + +def test_sync_stops_on_home_newer_managed_drift_via_bisync_review( + monkeypatch, tmp_path: Path, capsys +) -> None: + source_root = tmp_path / "source" + home_root = tmp_path / "home" + initialize_source_repo(source_root) + cli_module = sync_cli_module() + initial_plan = cli_module.build_home_sync_plan( + source_root=source_root, + home_root=home_root, + targets=cli_module.parse_targets("skills"), + mode="apply", + ) + cli_module.apply_home_sync_plan( + initial_plan, + create_missing_dirs=True, + ) + + source_skill = source_root / ".github/skills/demo-skill" + home_skill = home_root / ".agents/skills/demo-skill" + write_file(home_skill / "SKILL.md", "# Home edited\n") + set_tree_mtime(source_skill, 100.0) + set_tree_mtime(home_skill, 200.0) + + monkeypatch.setattr( + sync_home_ai_resources, + "parse_args", + lambda _=None: argparse.Namespace( + command="sync", + source_root=str(source_root), + home_root=str(home_root), + targets="skills", + retire_targets="", + create_missing_dirs=False, + prune_managed=False, + experimental_targets=False, + format="json", + fast=False, + changed_only=False, + ), + ) + + exit_code = sync_home_ai_resources.main() + payload = json.loads(capsys.readouterr().out) + + assert exit_code == 1 + assert payload["mode"] == "sync" + assert payload["status"] == "needs_review" + assert payload["install"]["blocked_codes"] == [] + assert any( + operation["action"] == "warning" + and operation["code"] == "target-modified-managed" + for operation in payload["install"]["operations"] + ) + assert any( + drift["direction"] == "home-to-repo" and drift["skill"] == "demo-skill" + for drift in payload["bisync"]["drifts"] + ) + assert (home_skill / "SKILL.md").read_text(encoding="utf-8") == "# Home edited\n" + + def test_skill_bundle_default_targets_focus_on_shared_skills( monkeypatch, tmp_path: Path, capsys ) -> None: @@ -529,7 +644,7 @@ def test_skill_bundle_default_targets_focus_on_shared_skills( create_missing_dirs=False, prune_managed=False, experimental_targets=False, - format="text", + format="report", fast=False, changed_only=False, ), @@ -564,6 +679,8 @@ def test_skill_runbook_distinguishes_install_and_bisync_lanes() -> None: assert "## Reporting Contract" in content assert "summary-first" in content assert "Auto-run safe repo-to-home install" in content + assert "repo wins" in content + assert "local-*" in content assert "Do not run `bisync apply` automatically" in content assert "remove it manually so sync can restore the source-of-truth version" not in ( content.lower() @@ -588,6 +705,7 @@ def test_sync_contract_documents_verified_bisync_reconciliation() -> None: assert "bisync-manifest-reconcile-failed" in contract_content assert "Text reports must use a summary-first layout" in contract_content assert "### Sync, Plan, Audit, And Bisync Plan Report" in contract_content + assert "Auto-applied" in contract_content assert "The `sync` command is the only auto-execute mode." in contract_content assert ( "| Resource or path | Lane | Planned action | Why this will change | Evidence or winner |" @@ -604,6 +722,47 @@ def test_sync_contract_documents_verified_bisync_reconciliation() -> None: ) +def test_sync_report_output_has_compact_chat_sections( + monkeypatch, tmp_path: Path, capsys +) -> None: + source_root = tmp_path / "source" + home_root = tmp_path / "home" + initialize_source_repo(source_root) + write_file(home_root / ".agents/skills/home-only/SKILL.md", "# Home only\n") + monkeypatch.setattr( + sync_home_ai_resources, + "parse_args", + lambda _=None: argparse.Namespace( + command="sync", + source_root=str(source_root), + home_root=str(home_root), + targets="skills", + retire_targets="", + create_missing_dirs=True, + prune_managed=False, + experimental_targets=False, + format="report", + fast=False, + changed_only=False, + ), + ) + + exit_code = sync_home_ai_resources.main() + output = capsys.readouterr().out + + assert exit_code == 1 + assert "Status: sync | status=needs_review" in output + assert "targets=skills" in output + assert "next_action=review_bisync" in output + assert "## Summary" in output + assert "## Auto-applied" in output + assert "## Stopped on" in output + assert "## Validation" in output + assert "## Next" in output + assert "# Install Lane" not in output + assert "# Bisync Lane" not in output + + def test_agent_and_skill_align_on_summary_first_reporting() -> None: agent_path = ( Path(__file__).resolve().parent @@ -700,6 +859,8 @@ def test_main_plan_report_output_has_stable_summary_sections( assert exit_code == 0 assert "## Summary" in output assert "repo-to-home install" in output + assert "targets=skills" in output + assert "next_action=apply" in output assert "## Changes" in output assert "## Attention" in output assert "## Validation" in output @@ -735,9 +896,144 @@ def test_bisync_plan_report_output_has_stable_summary_sections( assert exit_code == 1 assert "## Summary" in output assert "repo-home drift" in output + assert "next_action=resolve_blockers" in output assert "## Changes" in output assert "## Attention" in output assert "## Validation" in output assert "## Remaining Work" in output assert "## Next" in output assert "## Current State" not in output + + +def test_install_report_bounds_large_change_tables() -> None: + cli = sync_cli_module() + operations = [ + { + "action": "copy", + "resource_id": f"skill-{index:02d}", + "path": f"/tmp/home/.agents/skills/skill-{index:02d}", + "reason": "Repository copy is newer than home copy.", + "target": "skills", + } + for index in range(25) + ] + payload = { + "mode": "plan", + "selected_targets": ["skills"], + "retired_targets": [], + "source_resources_considered": 25, + "operations": operations, + "blocked_codes": [], + "validation": "ready", + "next_action": { + "action": "apply", + "allowed": True, + "requires_explicit_approval": True, + "command": "apply --targets skills", + "reason": "Plan is ready.", + }, + "next_step": "Run apply when ready.", + } + + output = cli.render_install_report(payload) + + assert "skill-00" in output + assert "skill-19" in output + assert "skill-20" not in output + assert ( + "5 additional change rows omitted; use --format json for full detail." in output + ) + + +def test_doctor_report_output_has_readiness_sections( + monkeypatch, tmp_path: Path, capsys +) -> None: + source_root = tmp_path / "source" + home_root = tmp_path / "home" + initialize_source_repo(source_root) + monkeypatch.setattr( + sync_home_ai_resources, + "parse_args", + lambda _=None: argparse.Namespace( + command="doctor", + source_root=str(source_root), + home_root=str(home_root), + targets="skills", + create_missing_dirs=False, + prune_managed=False, + experimental_targets=False, + format="report", + fast=False, + changed_only=False, + ), + ) + + exit_code = sync_home_ai_resources.main() + output = capsys.readouterr().out + + assert exit_code == 0 + assert "Status: repo-to-home install | mode=doctor | targets=skills" in output + assert "next_action=resolve_blockers" in output + assert "## Readiness" in output + assert "target-root:skills" in output + assert "needs-directory-create" in output + assert "## Next" in output + + +def test_report_tables_escape_pipe_and_newline_cells() -> None: + cli = sync_cli_module() + payload = { + "mode": "plan", + "selected_targets": ["skills"], + "retired_targets": [], + "source_resources_considered": 1, + "operations": [ + { + "action": "blocked", + "resource_id": "broken|skill", + "path": "/tmp/home/.agents/skills/broken|skill", + "reason": "First line\nSecond | part", + "code": "target-exists-unmanaged", + "target": "skills", + } + ], + "blocked_codes": ["target-exists-unmanaged"], + "validation": "blocked", + "next_action": { + "action": "resolve_blockers", + "allowed": False, + "requires_explicit_approval": True, + "command": "none", + "reason": "Resolve blockers.", + }, + "next_step": "Resolve blockers.", + } + + output = cli.render_install_report(payload) + + assert "broken\\|skill" in output + assert "First line Second \\| part" in output + + +def test_cli_defaults_to_report_output_for_user_facing_modes() -> None: + cli = sync_cli_module() + + assert cli.parse_args(["plan"]).format == "report" + assert cli.parse_args(["doctor"]).format == "report" + assert cli.parse_args(["bisync", "plan"]).format == "report" + + +def test_invalid_target_report_has_clear_blocked_next_action(capsys) -> None: + cli = sync_cli_module() + args = cli.parse_args(["plan", "--targets", "nope"]) + + exit_code = cli.run(args) + output = capsys.readouterr().out + + assert exit_code == 1 + assert ( + "Status: repo-to-home install | mode=plan | targets=none | status=blocked" + in output + ) + assert "next_action=resolve_blockers" in output + assert "unknown-target" in output diff --git a/tests/test_imported_asset_overrides_contract.py b/tests/test_imported_asset_overrides_contract.py index d834e49c..b80a199d 100644 --- a/tests/test_imported_asset_overrides_contract.py +++ b/tests/test_imported_asset_overrides_contract.py @@ -73,10 +73,15 @@ def test_imported_asset_override_policy_is_visible_in_canonical_and_sync_assets( ".github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md" ).read_text(encoding="utf-8") - assert "Sync agents own catalog prefix rules" in agents_text + assert ( + "Source-side sync command centers and sync support skills own propagation behavior." + in agents_text + ) + assert "Keep consumer-facing defaults target-agnostic." in agents_text + assert "Do not treat this file as instructions for coding agents" in copilot_text assert ( "Do not edit imported upstream assets in place unless the need is strong" - in copilot_text + not in copilot_text ) assert "Every approved imported in-place override must be mapped" in sync_agent_text assert ( diff --git a/tests/test_plan_policy_contract.py b/tests/test_plan_policy_contract.py index d5844000..09c97fd2 100644 --- a/tests/test_plan_policy_contract.py +++ b/tests/test_plan_policy_contract.py @@ -7,13 +7,11 @@ def read_text(relative_path: str) -> str: return Path(relative_path).read_text(encoding="utf-8") -def test_root_policy_files_keep_retained_plan_defaults_outside_always_on_detail() -> ( - None -): +def test_root_policy_files_keep_retained_plan_paths_outside_always_on_detail() -> None: agents_text = read_text("AGENTS.md") copilot_text = read_text(".github/copilot-instructions.md") - assert "tmp/superpowers/" in agents_text - assert "retained-plan" in agents_text + assert "tmp/superpowers/" not in agents_text + assert "retained-plan" not in agents_text assert "tmp/superpowers/" not in copilot_text @@ -108,3 +106,26 @@ def test_gateway_skills_preserve_compact_context_discipline() -> None: assert "Escalation trigger" in simple_text assert "Preserve known-context handoff quality" in writing_text assert "run targeted rereads" in writing_text + + +def test_writing_plans_counter_validation_contract_is_explicit() -> None: + writing_text = read_text(".github/skills/internal-gateway-writing-plans/SKILL.md") + compact_reference = read_text( + ".github/skills/internal-gateway-writing-plans/references/compact-plan-contract.md" + ) + review_gate = read_text( + ".github/skills/internal-gateway-writing-plans/references/plan-review-gate.md" + ) + scope_challenge = read_text( + ".github/skills/internal-gateway-writing-plans/references/scope-challenge.md" + ) + + assert "counter-validation-critical facts" in writing_text + assert "counter-validation-critical facts" in compact_reference + assert ( + "counter-validate coverage without reading `02-execution.md` or `02-control.md`" + in review_gate + ) + assert ( + "counter-validate the plan without reading the control file" in scope_challenge + ) diff --git a/tests/test_repository_workflow_policy_contract.py b/tests/test_repository_workflow_policy_contract.py index 116bbf31..ae591160 100644 --- a/tests/test_repository_workflow_policy_contract.py +++ b/tests/test_repository_workflow_policy_contract.py @@ -59,41 +59,57 @@ def test_root_files_define_scoped_instruction_loading_for_manual_runtimes() -> N agents_text = read_text("AGENTS.md") copilot_text = read_text(".github/copilot-instructions.md") - assert "## Rule Placement" in agents_text - assert "operational procedures, checklists, file-shape recipes" in agents_text - assert "smallest valid owner" in agents_text - - assert "## Context And Scope" in agents_text - assert "select the smallest relevant skill" in agents_text - assert "then read that `SKILL.md` as manual context" in agents_text - assert "Load umbrella domain skills before specialist depth" in agents_text - assert "Co-load specialist skills or bundle references only when" in agents_text - assert "Use the smallest valid owner to resolve conflicts" in agents_text + assert "## First Move" in agents_text + assert "## Precedence" in agents_text + assert "Resolve conflicts with the smallest valid owner." in agents_text + assert ( + "fallback policy, not permission to override narrower contracts." in agents_text + ) - assert "Select the smallest relevant skill from the prompt" in copilot_text - assert "validation signal, or repository evidence" in copilot_text + assert "## Scope And Placement" in agents_text + assert "operational procedures, checklists, file-shape recipes" in agents_text + assert ".github/instructions" not in agents_text + assert "bridge" not in agents_text.lower() + + assert "``" in agents_text + assert "This block is the portable baseline" in agents_text + assert "``" in agents_text + assert "This block applies only to this standards repository." in agents_text + + assert "It is not a general task-execution guide" in copilot_text + assert "Do not ask the author to follow local runtime workflows" in copilot_text + assert "that GitHub.com cannot" in copilot_text + assert "execute." in copilot_text assert ( - "Load task-specific skills or references only when workflow depth" + "Do not treat this file as instructions for coding agents, local CLIs, or" in copilot_text ) -def test_agents_external_tool_routing_block_is_legitimized_and_source_local() -> None: +def test_agents_keeps_only_intentional_root_level_workflows() -> None: agents_text = read_text("AGENTS.md") + assert "tool-specific workflows here" in agents_text + assert "Do not duplicate skill-owned paths" in agents_text + assert "## graphify" in agents_text + assert "When the user types `/graphify`" in agents_text + assert 'graphify query ""' in agents_text assert ( - "An external tool that reads this file may require a clearly delimited, " - "trailing routing block carrying the minimal commands it needs to consume " - "the repository; keep any such block source-local, outside the governed " - "policy blocks, and limited to that tool's required invocation anchors." + "Only skip graphify if the task is about stale or incorrect graph output" in agents_text ) - assert "## graphify" in agents_text - local_rules_close_index = agents_text.index("``") - graphify_index = agents_text.index("## graphify") - assert graphify_index > local_rules_close_index +def test_internal_contract_tracks_current_root_policy_shape() -> None: + internal_contract_text = read_text("INTERNAL_CONTRACT.md") + + assert "portable `` block" in internal_contract_text + assert "`` block" in internal_contract_text + assert "compact graph orientation rules" in internal_contract_text + assert ( + "instead of assuming a separate `## Context Routing` section" + in internal_contract_text + ) def test_agents_tactical_defaults_include_compact_context_discipline() -> None: diff --git a/tests/test_retained_learning_contract.py b/tests/test_retained_learning_contract.py index 4dca7310..9b9fcfbe 100644 --- a/tests/test_retained_learning_contract.py +++ b/tests/test_retained_learning_contract.py @@ -12,16 +12,6 @@ def read_text(relative_path: str) -> str: def test_retained_learning_contract_preserves_on_disk_lessons_rows() -> None: expected_contracts: dict[str, list[str]] = { - "AGENTS.md": [ - "`tmp/superpowers/` and `LESSONS_LEARNED.md` may hold retained work", - "Treat retained plans and retained learning as non-canonical", - "Keep file shape, execution workflow, and ledger row rules in retained-plan skills or owned files", - ], - ".github/copilot-instructions.md": [ - "Treat retained plans and `LESSONS_LEARNED.md` as non-canonical", - "dedicated retained-plan skills and lesson-codification owners", - "ledger row rules", - ], ".github/skills/internal-lesson-codification/SKILL.md": [ "This skill owns the codification workflow.", "ledger row format, which stays in `LESSONS_LEARNED.md`", @@ -50,6 +40,17 @@ def test_retained_learning_contract_preserves_on_disk_lessons_rows() -> None: f"{relative_path} is missing retained-learning guardrail text: {snippet}" ) + copilot_text = read_text(".github/copilot-instructions.md") + assert "This file is only for GitHub.com Copilot code review." in copilot_text + assert ( + "Do not treat this file as instructions for coding agents, local CLIs" + in copilot_text + ) + + agents_text = read_text("AGENTS.md") + assert "LESSONS_LEARNED.md" not in agents_text + assert "ledger row" not in agents_text + def test_pending_lesson_rows_have_expected_shape_when_present() -> None: lines = read_text("LESSONS_LEARNED.md").splitlines() diff --git a/tests/test_retained_plan_artifact_contract.py b/tests/test_retained_plan_artifact_contract.py index 90aa3933..a7d6e891 100644 --- a/tests/test_retained_plan_artifact_contract.py +++ b/tests/test_retained_plan_artifact_contract.py @@ -106,16 +106,21 @@ def completed_retained_plan_violations(root: Path) -> list[str]: continuation_match = re.search( r"^Continuation:\s*(.+)$", plan_state_text, re.MULTILINE ) + user_action_match = re.search( + r"^User action required:\s*(.+)$", plan_state_text, re.MULTILINE + ) + next_step_match = re.search( + r"^Next-step package:\s*(.+)$", plan_state_text, re.MULTILINE + ) + evidence_gaps_match = re.search( + r"^Evidence gaps:\s*(.+)$", plan_state_text, re.MULTILINE + ) + declared_state: str | None = None if state_match is None: violations.append(f"{plan_state_path} is missing State") - elif state_match.group(1).strip() != "DONE": - violations.append(f"{plan_state_path} must declare State: DONE") - - if marker_state and marker_state != "DONE": - violations.append( - f"{plan_state_path} must encode DONE state in filename" - ) + else: + declared_state = state_match.group(1).strip().upper() if ( marker_state @@ -128,8 +133,32 @@ def completed_retained_plan_violations(root: Path) -> list[str]: if continuation_match is None: violations.append(f"{plan_state_path} is missing Continuation") - elif continuation_match.group(1).strip() != "none": - violations.append(f"{plan_state_path} must declare Continuation: none") + elif declared_state is not None: + continuation = continuation_match.group(1).strip() + + if declared_state == "DONE": + if continuation != "none": + violations.append( + f"{plan_state_path} must declare Continuation: none" + ) + else: + if continuation not in {"continuing", "waiting"}: + violations.append( + f"{plan_state_path} must declare Continuation: continuing or waiting" + ) + + if continuation == "waiting" and user_action_match is None: + violations.append( + f"{plan_state_path} is missing User action required" + ) + + if next_step_match is None: + violations.append( + f"{plan_state_path} is missing Next-step package" + ) + + if evidence_gaps_match is None: + violations.append(f"{plan_state_path} is missing Evidence gaps") continue active_files = numbered_plan_files(folder) @@ -225,6 +254,21 @@ def write_lightweight_completed_plan_folder(root: Path) -> Path: return plan_folder +def write_blocked_plan_folder(root: Path) -> Path: + plan_folder = root / "sample-plan" + plan_folder.mkdir() + (plan_folder / "BLOCKED-plan-state.md").write_text( + "Plan State\n" + "State: BLOCKED\n" + "Continuation: waiting\n" + "User action required: approve next owner\n" + "Next-step package: Owner=internal-markdown; Scope=state marker refresh; Action=resume plan; Validation=pytest; Risk=stale evidence\n" + "Evidence gaps: validator remains red\n", + encoding="utf-8", + ) + return plan_folder + + def test_completed_retained_plan_validation_accepts_well_formed_folder( tmp_path: Path, ) -> None: @@ -241,6 +285,14 @@ def test_completed_retained_plan_validation_accepts_lightweight_folder( assert completed_retained_plan_violations(tmp_path) == [] +def test_completed_retained_plan_validation_accepts_active_blocked_marker( + tmp_path: Path, +) -> None: + write_blocked_plan_folder(tmp_path) + + assert completed_retained_plan_violations(tmp_path) == [] + + def test_completed_retained_plan_validation_rejects_active_numbered_files( tmp_path: Path, ) -> None: @@ -279,9 +331,26 @@ def test_completed_retained_plan_validation_rejects_lightweight_non_shipped( ) assert completed_retained_plan_violations(tmp_path) == [ - f"{plan_folder / 'DONE-plan-state.md'} must declare State: DONE", f"{plan_folder / 'DONE-plan-state.md'} filename state does not match declared State", - f"{plan_folder / 'DONE-plan-state.md'} must declare Continuation: none", + f"{plan_folder / 'DONE-plan-state.md'} is missing Next-step package", + f"{plan_folder / 'DONE-plan-state.md'} is missing Evidence gaps", + ] + + +def test_completed_retained_plan_validation_rejects_waiting_marker_missing_fields( + tmp_path: Path, +) -> None: + plan_folder = tmp_path / "sample-plan" + plan_folder.mkdir() + (plan_folder / "BLOCKED-plan-state.md").write_text( + "Plan State\nState: BLOCKED\nContinuation: waiting\n", + encoding="utf-8", + ) + + assert completed_retained_plan_violations(tmp_path) == [ + f"{plan_folder / 'BLOCKED-plan-state.md'} is missing User action required", + f"{plan_folder / 'BLOCKED-plan-state.md'} is missing Next-step package", + f"{plan_folder / 'BLOCKED-plan-state.md'} is missing Evidence gaps", ] diff --git a/tests/test_sync_and_token_risks.py b/tests/test_sync_and_token_risks.py index 42ed91cc..bc31c697 100644 --- a/tests/test_sync_and_token_risks.py +++ b/tests/test_sync_and_token_risks.py @@ -102,7 +102,7 @@ def test_build_sync_plan_preserves_local_assets_and_deletes_non_local_assets( assert ("delete", ".github/agents/internal-sync-legacy.agent.md") in actions -def test_build_sync_plan_ships_source_instruction_family( +def test_build_sync_plan_does_not_ship_source_instruction_family( tmp_path: Path, ) -> None: source_root = tmp_path / "source" @@ -120,10 +120,12 @@ def test_build_sync_plan_ships_source_instruction_family( plan = build_sync_plan(source_root, target_root) planned_paths = {operation.path for operation in plan.operations} - assert f"{LEGACY_INSTRUCTION_DIR}/internal-python.instructions.md" in planned_paths + assert ( + f"{LEGACY_INSTRUCTION_DIR}/internal-python.instructions.md" not in planned_paths + ) -def test_build_sync_plan_updates_target_instructions_and_preserves_local( +def test_build_sync_plan_deletes_target_non_local_instructions_and_preserves_local( tmp_path: Path, ) -> None: source_root = tmp_path / "source" @@ -150,7 +152,7 @@ def test_build_sync_plan_updates_target_instructions_and_preserves_local( actions = {(operation.action, operation.path) for operation in plan.operations} assert ( - "update", + "delete", f"{LEGACY_INSTRUCTION_DIR}/internal-python.instructions.md", ) in actions assert ( @@ -159,7 +161,7 @@ def test_build_sync_plan_updates_target_instructions_and_preserves_local( ) in actions -def test_build_sync_plan_ships_source_managed_instructions_and_preserves_local( +def test_build_sync_plan_does_not_recreate_source_instructions_after_cleanup( tmp_path: Path, ) -> None: source_root = tmp_path / "source" @@ -187,9 +189,13 @@ def test_build_sync_plan_ships_source_managed_instructions_and_preserves_local( actions = {(operation.action, operation.path) for operation in plan.operations} assert ( - "update", + "delete", f"{LEGACY_INSTRUCTION_DIR}/internal-python.instructions.md", ) in actions + assert ( + "create", + f"{LEGACY_INSTRUCTION_DIR}/internal-python.instructions.md", + ) not in actions assert ( "preserve", f"{LEGACY_INSTRUCTION_DIR}/local-team.instructions.md", @@ -1111,12 +1117,12 @@ def test_apply_sync_manifest_tracks_vscode_settings_without_file_hash( ) -def test_detect_token_risks_reports_bridge_overlap(tmp_path: Path) -> None: +def test_detect_token_risks_reports_root_policy_overlap(tmp_path: Path) -> None: repeated_lines = "\n".join( [ "- Keep policy separate from inventory.", "- Keep AGENTS.md strategic and stable.", - "- Keep .github/copilot-instructions.md as the projection layer.", + "- Keep .github/copilot-instructions.md review-only.", "- Keep .github/INVENTORY.md as the exact catalog.", "- Preserve explicit precedence rules.", "- Remove overlap instead of keeping compatibility copies.", @@ -1133,7 +1139,7 @@ def test_detect_token_risks_reports_bridge_overlap(tmp_path: Path) -> None: findings = detect_token_risks(tmp_path) finding_codes = {finding.code for finding in findings} - assert "bridge-overlap" in finding_codes + assert "root-policy-overlap" in finding_codes def test_detect_token_risks_reports_root_always_on_budget(tmp_path: Path) -> None: @@ -1230,17 +1236,16 @@ def test_detect_token_risks_reports_review_baseline_window_missing_core_rules( assert "review-baseline-window-missing-core-rules" in finding_codes -def test_detect_token_risks_ignores_structural_bridge_references( +def test_detect_token_risks_ignores_structural_root_policy_references( tmp_path: Path, ) -> None: write_file( tmp_path / "AGENTS.md", "# AGENTS\n\n" - "- Use `.github/copilot-instructions.md` as the compact repo-wide bridge.\n" + "- Use `AGENTS.md` as the repository agent entrypoint.\n" "- Use `.github/INVENTORY.md` as the live catalog.\n" "- Use `.github/skills/` when a reusable workflow or technical baseline is relevant.\n" - "- Use `.github/agents/` when a stable owner is relevant.\n" - "- Keep local exceptions in `.github/instructions/local-*.instructions.md`.\n", + "- Use `.github/agents/` when a stable owner is relevant.\n", ) write_file(tmp_path / ".github/copilot-instructions.md", "# Copilot\n") write_file(tmp_path / ".github/INVENTORY.md", "# Inventory\n") @@ -1248,7 +1253,7 @@ def test_detect_token_risks_ignores_structural_bridge_references( findings = detect_token_risks(tmp_path) finding_codes = {finding.code for finding in findings} - assert "inventory-dump-in-bridge" not in finding_codes + assert "inventory-dump-in-root-policy" not in finding_codes def test_detect_token_risks_reports_internal_root_policy_overlap( diff --git a/tests/test_workflow_review_contract.py b/tests/test_workflow_review_contract.py index 783c827c..61ec4e8d 100644 --- a/tests/test_workflow_review_contract.py +++ b/tests/test_workflow_review_contract.py @@ -31,11 +31,19 @@ def test_idea_gateway_owns_retained_planning() -> None: assert "Plan Approval Gate 3: waiting" in reference_text assert "Handoff Gate 4: plan-created" in reference_text assert "Specialization Checkpoint: gated" in reference_text - assert "ask whether the user wants this owner to keep the task" in reference_text + assert "Direct Execution vs Retained Plan Recommendation" in skill_text + assert "direct execution via `internal-gateway-simple-task`" in skill_text + assert "Recommendation`, `Why`, `Tradeoff`, and `Decision`" in reference_text assert "mini-plan" in reference_text assert "go/ok/procedi" in runtime_text assert "Specialization Checkpoint: gated" in runtime_text - assert "ask whether the user wants this owner to keep the task" in runtime_text + assert "Direct Execution vs Retained Plan Recommendation" in runtime_text + assert "choose execute, plan, or an explicit override" in runtime_text + assert "ask whether the user wants this owner to keep the task" not in skill_text + assert ( + "ask whether the user wants this owner to keep the task" not in reference_text + ) + assert "ask whether the user wants this owner to keep the task" not in runtime_text assert ( "At Interview Gate 1: ready-for-critical, ask whether to continue" in runtime_text @@ -64,6 +72,42 @@ def test_review_gateway_exists_and_stops_before_fixes() -> None: assert "counter-validation" in skill_text assert "counter-validation" in review_gate_lower assert "route or next owner" in review_gate_lower + assert "decision-usefulness" in review_gate_lower + assert "accept, patch, investigate, plan" in review_gate_lower + + +def test_review_usefulness_contract_is_explicit() -> None: + gateway_text = read_text(".github/skills/internal-gateway-review/SKILL.md") + ai_review_text = read_text(".github/skills/internal-ai-resource-review/SKILL.md") + report_text = read_text( + ".github/skills/internal-ai-resource-review/references/report-contract.md" + ) + profile_text = read_text( + ".github/skills/internal-ai-resource-review/references/review-profiles.md" + ) + replay_text = read_text( + ".github/skills/internal-ai-resource-review/references/review-usefulness-replay-fixture.md" + ) + + assert "decision-usefulness" in gateway_text + assert "clear next decision" in gateway_text + assert "Bundle coverage rules" in ai_review_text + assert "review-usefulness-replay-fixture.md" in ai_review_text + assert ( + "live prompt pack, generated artifact, retained report, or fixture" + in ai_review_text + ) + assert "evidence digest or decision trace" in ai_review_text + assert "Adaptive layout patterns" in report_text + assert "Evidence compression" in report_text + assert "Missing proof handling" in report_text + assert "test-gap" in report_text + assert "runtime-artifact" in report_text + assert "No-finding and low-finding reviews" in report_text + assert "Report coverage separately from findings" in profile_text + assert "coach-personale" in replay_text + assert "Focused pytest execution was unavailable" in replay_text + assert "Does not invent additional findings" in replay_text def test_compact_and_extended_execution_owner_is_unified() -> None: @@ -103,6 +147,25 @@ def test_gateway_compliance_audit_contract_is_explicit() -> None: assert "stdlib-only CLI launcher" in executing_text +def test_simple_gateway_direct_execution_control_contract_is_explicit() -> None: + simple_text = read_text(".github/skills/internal-gateway-simple-task/SKILL.md") + lanes_text = read_text( + ".github/skills/internal-gateway-simple-task/references/simple-lanes.md" + ) + runtime_text = read_text( + ".github/skills/internal-gateway-simple-task/agents/openai.yaml" + ) + + assert "Direct Execution Control" in simple_text + assert "Direct Completion Control" in simple_text + assert "original intent, separated from emerged requirements" in simple_text + assert "all in-scope source items" in runtime_text + assert "mandatory applicable requirements are closed" in runtime_text + assert "direct-control status" in lanes_text + assert "One successful validator" in simple_text + assert "emerged in-scope requirements" in simple_text + + def test_critical_master_claim_discipline_contract() -> None: critical_text = read_text( ".github/skills/internal-gateway-critical-master/SKILL.md" diff --git a/tests/tools/analyze_copilot_debug_log/test_entrypoint.py b/tests/tools/analyze_copilot_debug_log/test_entrypoint.py index 33ea69cd..f1259f3f 100644 --- a/tests/tools/analyze_copilot_debug_log/test_entrypoint.py +++ b/tests/tools/analyze_copilot_debug_log/test_entrypoint.py @@ -187,6 +187,107 @@ def test_prompt_exports_subcommand_summarizes_and_dedupes(tmp_path: Path) -> Non assert "PRIVATE USER BODY" not in result.stdout +def test_prompt_exports_reports_sequence_cost_diagnostics(tmp_path: Path) -> None: + prompt_export = { + "prompts": [ + { + "promptId": "prompt-sequence", + "logs": [ + { + "id": "request-start", + "kind": "request", + "metadata": { + "model": "gpt-test", + "usage": { + "prompt_tokens": 100_000, + "prompt_tokens_details": {"cached_tokens": 95_000}, + "completion_tokens": 100, + }, + }, + }, + { + "id": "patch-big", + "kind": "toolCall", + "tool": "apply_patch", + "args": "A" * 12_000, + "response": "patched", + }, + { + "id": "request-after-patch", + "kind": "request", + "metadata": { + "model": "gpt-test", + "usage": { + "prompt_tokens": 114_000, + "prompt_tokens_details": {"cached_tokens": 90_000}, + "completion_tokens": 50, + }, + }, + }, + { + "id": "tiny-tool", + "kind": "toolCall", + "tool": "manage_todo_list", + "args": {"todoList": []}, + "response": "ok", + }, + { + "id": "request-cache-drop", + "kind": "request", + "metadata": { + "model": "gpt-test", + "usage": { + "prompt_tokens": 114_500, + "prompt_tokens_details": {"cached_tokens": 70_000}, + "completion_tokens": 25, + }, + }, + }, + ], + } + ] + } + prompt_path = tmp_path / "prompt-sequence.json" + prompt_path.write_text(json.dumps(prompt_export), encoding="utf-8") + + result = run_tool("prompt-exports", str(prompt_path)) + payload = json.loads(result.stdout) + + prompt_summary = payload["prompts"][0] + top_spike = prompt_summary["top_non_cached_spikes"][0] + assert top_spike == { + "prompt_id": "prompt-sequence", + "request_id": "request-cache-drop", + "model": "gpt-test", + "prompt_tokens": 114_500, + "cached_tokens": 70_000, + "non_cached_input_tokens": 44_500, + "previous_cached_tokens": 90_000, + "cache_delta_tokens": -20_000, + "prompt_delta_tokens": 500, + "previous_tool_payload_bytes": 17, + "previous_tool_names": ["manage_todo_list"], + } + assert prompt_summary["cache_drop_events"] == [top_spike] + + payload_candidate = prompt_summary["payload_to_noncache_candidates"][0] + assert payload_candidate["request_id"] == "request-after-patch" + assert payload_candidate["previous_tool_names"] == ["apply_patch"] + assert payload_candidate["previous_tool_payload_bytes"] >= 12_000 + + aggregate = payload["aggregate"] + assert aggregate["top_non_cached_spikes"][0] == top_spike + assert aggregate["cache_drop_events"][0] == top_spike + assert aggregate["payload_to_noncache_candidates"][0] == payload_candidate + + markdown_result = run_tool( + "prompt-exports", str(prompt_path), "--format", "markdown" + ) + assert "## Sequence Diagnostics" in markdown_result.stdout + assert "prompt-sequence/request-cache-drop" in markdown_result.stdout + assert "previous tools manage_todo_list" in markdown_result.stdout + + def test_debug_logs_subcommand_summarizes_otlp_and_dedupes_prompt_exports( tmp_path: Path, ) -> None: diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 00000000..843a8f07 --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1 @@ +"""Repository tools package.""" diff --git a/tools/analyze_copilot_debug_log/prompt_exports.py b/tools/analyze_copilot_debug_log/prompt_exports.py index 72347f40..0429774e 100644 --- a/tools/analyze_copilot_debug_log/prompt_exports.py +++ b/tools/analyze_copilot_debug_log/prompt_exports.py @@ -37,6 +37,9 @@ } ) SYSTEM_ROLES = frozenset({"0", "system", "developer"}) +SEQUENCE_DIAGNOSTIC_LIMIT = 10 +CACHE_DROP_EVENT_THRESHOLD_TOKENS = 8_000 +PAYLOAD_TO_NONCACHE_TOOL_BYTES_THRESHOLD = 10_000 VOLATILE_KEYS = frozenset( { @@ -413,6 +416,8 @@ def summarize_prompt_log(log: dict[str, object]) -> dict[str, object]: ) return { + "log_id": first_str(log, "id", "spanId", "span_id", "name"), + "model": first_str(metadata, "model"), "prompt_tokens": prompt_tokens, "cached_tokens": cached_tokens, "non_cached_input_tokens": max(prompt_tokens - cached_tokens, 0), @@ -440,6 +445,119 @@ def summarize_prompt_log(log: dict[str, object]) -> dict[str, object]: } +def is_token_request_summary(summary: dict[str, object]) -> bool: + return not bool(summary.get("tool_call")) and any( + as_int(summary.get(key)) + for key in ( + "prompt_tokens", + "cached_tokens", + "completion_tokens", + "reasoning_tokens", + ) + ) + + +def sequence_diagnostic_record( + *, + prompt_id: str, + current: dict[str, object], + previous: dict[str, object], + previous_tool_payload_bytes: int, + previous_tool_names: list[str], +) -> dict[str, object]: + cached_tokens = as_int(current.get("cached_tokens")) + previous_cached_tokens = as_int(previous.get("cached_tokens")) + prompt_tokens = as_int(current.get("prompt_tokens")) + previous_prompt_tokens = as_int(previous.get("prompt_tokens")) + return { + "prompt_id": prompt_id, + "request_id": str(current.get("log_id") or ""), + "model": str(current.get("model") or ""), + "prompt_tokens": prompt_tokens, + "cached_tokens": cached_tokens, + "non_cached_input_tokens": as_int(current.get("non_cached_input_tokens")), + "previous_cached_tokens": previous_cached_tokens, + "cache_delta_tokens": cached_tokens - previous_cached_tokens, + "prompt_delta_tokens": prompt_tokens - previous_prompt_tokens, + "previous_tool_payload_bytes": previous_tool_payload_bytes, + "previous_tool_names": previous_tool_names, + } + + +def top_sequence_records( + records: list[dict[str, object]], key: str, *, reverse: bool = True +) -> list[dict[str, object]]: + return sorted( + records, + key=lambda item: ( + as_int(item.get(key)), + as_int(item.get("previous_tool_payload_bytes")), + str(item.get("request_id") or ""), + ), + reverse=reverse, + )[:SEQUENCE_DIAGNOSTIC_LIMIT] + + +def summarize_sequence_diagnostics( + log_summaries: list[dict[str, object]], *, prompt_id: str +) -> dict[str, object]: + records: list[dict[str, object]] = [] + previous_request: dict[str, object] | None = None + previous_tool_payload_bytes = 0 + previous_tool_names: list[str] = [] + + for summary in log_summaries: + if summary.get("tool_call"): + previous_tool_payload_bytes += as_int(summary.get("tool_payload_bytes")) + tool_name = str(summary.get("tool_name") or "") + if tool_name: + previous_tool_names.append(tool_name) + continue + + if not is_token_request_summary(summary): + continue + + if previous_request is not None: + records.append( + sequence_diagnostic_record( + prompt_id=prompt_id, + current=summary, + previous=previous_request, + previous_tool_payload_bytes=previous_tool_payload_bytes, + previous_tool_names=previous_tool_names, + ) + ) + + previous_request = summary + previous_tool_payload_bytes = 0 + previous_tool_names = [] + + return { + "top_non_cached_spikes": top_sequence_records( + records, "non_cached_input_tokens" + ), + "cache_drop_events": top_sequence_records( + [ + record + for record in records + if as_int(record.get("cache_delta_tokens")) + <= -CACHE_DROP_EVENT_THRESHOLD_TOKENS + ], + "cache_delta_tokens", + reverse=False, + ), + "payload_to_noncache_candidates": top_sequence_records( + [ + record + for record in records + if as_int(record.get("previous_tool_payload_bytes")) + >= PAYLOAD_TO_NONCACHE_TOOL_BYTES_THRESHOLD + ], + "previous_tool_payload_bytes", + ), + } + + def summarize_prompt_export( prompt_export: dict[str, object], *, source_name: str ) -> list[dict[str, object]]: @@ -544,12 +662,18 @@ def summarize_prompt_export( if as_int(record["occurrences"]) > 1 and bool(record["retry_hint"]) ] composition = summarize_prompt_composition(logs) + prompt_id = ( + first_str(prompt, "promptId", "prompt_id", "id", "title") + or f"prompt-{index}" + ) + sequence_diagnostics = summarize_sequence_diagnostics( + log_summaries, prompt_id=prompt_id + ) summaries.append( { "source_name": source_name, - "prompt_id": first_str(prompt, "promptId", "prompt_id", "id", "title") - or f"prompt-{index}", + "prompt_id": prompt_id, "title": first_str(prompt, "title", "name") or f"prompt-{index}", "request_count": len(logs), "prompt_tokens": prompt_tokens_total, @@ -572,6 +696,7 @@ def summarize_prompt_export( "tool_counts_by_name": dict(sorted(tool_counts_by_name.items())), "top_tool_payloads": top_tool_payloads, "retry_like_duplicate_records": retry_like_duplicate_records, + **sequence_diagnostics, "composition": composition, } ) @@ -594,6 +719,9 @@ def aggregate_summaries(summaries: list[dict[str, object]]) -> dict[str, object] system_hash_bytes: dict[str, int] = {} skill_descriptions_by_key: dict[tuple[str, str], dict[str, object]] = {} duplicate_attachment_counts: Counter[str] = Counter() + top_non_cached_spikes: list[dict[str, object]] = [] + cache_drop_events: list[dict[str, object]] = [] + payload_to_noncache_candidates: list[dict[str, object]] = [] max_prompt_tokens = 0 first_context_tokens = 0 last_context_tokens = 0 @@ -619,6 +747,15 @@ def aggregate_summaries(summaries: list[dict[str, object]]) -> dict[str, object] for record in read_list(summary.get("retry_like_duplicate_records")): if isinstance(record, dict): retry_like_duplicate_records.append(record) + for record in read_list(summary.get("top_non_cached_spikes")): + if isinstance(record, dict): + top_non_cached_spikes.append(record) + for record in read_list(summary.get("cache_drop_events")): + if isinstance(record, dict): + cache_drop_events.append(record) + for record in read_list(summary.get("payload_to_noncache_candidates")): + if isinstance(record, dict): + payload_to_noncache_candidates.append(record) composition = read_dict(summary.get("composition")) for item in read_list(composition.get("repeated_system_message_hashes")): if isinstance(item, dict): @@ -708,6 +845,15 @@ def aggregate_summaries(summaries: list[dict[str, object]]) -> dict[str, object] ), "tool_counts_by_name": dict(sorted(tool_counts_by_name.items())), "top_tool_payloads": top_tool_payloads, + "top_non_cached_spikes": top_sequence_records( + top_non_cached_spikes, "non_cached_input_tokens" + ), + "cache_drop_events": top_sequence_records( + cache_drop_events, "cache_delta_tokens", reverse=False + ), + "payload_to_noncache_candidates": top_sequence_records( + payload_to_noncache_candidates, "previous_tool_payload_bytes" + ), "retry_like_duplicate_count": len(retry_like_duplicate_records), "retry_like_duplicate_records": retry_like_duplicate_records, "composition": { @@ -814,6 +960,30 @@ def build_report(paths: list[Path]) -> dict[str, object]: } +def format_sequence_diagnostic_item(item: dict[str, object]) -> str: + tool_names = read_list(item.get("previous_tool_names")) + tools = ", ".join(str(tool_name) for tool_name in tool_names) or "none" + return ( + f"{item.get('prompt_id', '')}/{item.get('request_id', '')}: " + f"non-cache {item.get('non_cached_input_tokens', 0)} tokens; " + f"cache delta {item.get('cache_delta_tokens', 0)}; " + f"previous tools {tools}; " + f"previous tool payload {item.get('previous_tool_payload_bytes', 0)} bytes" + ) + + +def append_sequence_diagnostic_section( + lines: list[str], title: str, records: list[object] +) -> None: + lines.append(f"### {title}") + if not records: + lines.append("- None") + return + for item in records[:5]: + if isinstance(item, dict): + lines.append(f"- {format_sequence_diagnostic_item(item)}") + + def format_markdown(report: dict[str, object]) -> str: aggregate = read_dict(report.get("aggregate")) composition = read_dict(aggregate.get("composition")) @@ -848,6 +1018,23 @@ def format_markdown(report: dict[str, object]) -> str: f"- {item.get('tool_name', '')}: {item.get('payload_bytes', 0)} bytes across {item.get('call_count', 0)} calls" ) lines.append("") + lines.append("## Sequence Diagnostics") + append_sequence_diagnostic_section( + lines, + "Top Non-Cached Spikes", + read_list(aggregate.get("top_non_cached_spikes")), + ) + append_sequence_diagnostic_section( + lines, + "Cache Drop Events", + read_list(aggregate.get("cache_drop_events")), + ) + append_sequence_diagnostic_section( + lines, + "Payload To Non-Cache Candidates", + read_list(aggregate.get("payload_to_noncache_candidates")), + ) + lines.append("") lines.append("## Prompt Composition") lines.append(f"- System messages: {composition.get('system_message_count', 0)}") lines.append(