diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 7dc9784..0094958 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -7,14 +7,15 @@ Use this format for new updates: - Include file/path scope when useful. ## 2026-04-12 +- Renamed the repository-root retained-learning ledger to `LESSONS_LEARNED.md`, then realigned the retained-learning contract, sync automation, and tests to use the new canonical path. - Aligned `.pre-commit-config.yaml` and expanded `.editorconfig` with file-type defaults for Python, shell, Terraform/HCL, YAML, JSON/TOML, Markdown, Make, and local config files so the repo and synced consumers get a practical editor baseline without the formatter ping-pong that left `pre-commit` failing with no visible git diff. - Expanded the cross-repository sync baseline to include `.editorconfig`, `.pre-commit-config.yaml`, and `.github/workflows/terraform-pre-commit.yml`, then updated the sync agent/skill contract and sync planner tests to keep that scope explicit and narrow. - Renamed the workflow skill from `internal-cicd-workflow` to `internal-github-actions`, renamed `internal-github-composite-action` to `internal-github-action-composite`, and realigned the GitHub Actions instructions so the umbrella instruction is the family baseline while the composite instruction now carries only composite-specific delta guidance. -- Added a retained-learning governance contract: root `AGENTS.md` now defines repository-root `LESSONS.md` as a non-canonical ledger for durable lessons learned during completed tasks, `.github/copilot-instructions.md` projects the same behavior into native Copilot flows, `INTERNAL_CONTRACT.md` source-governs the invariant, and the new `LESSONS.md` file records retained lessons with canonical-owner pointers. +- Added a retained-learning governance contract: root `AGENTS.md` now defines repository-root `LESSONS_LEARNED.md` as a non-canonical ledger for durable lessons learned during completed tasks, `.github/copilot-instructions.md` projects the same behavior into native Copilot flows, `INTERNAL_CONTRACT.md` source-governs the invariant, and the new `LESSONS_LEARNED.md` file records retained lessons with canonical-owner pointers. - Slimmed the retained-learning section in root `AGENTS.md` so the bridge keeps only strategic ownership and boundary language while `.github/copilot-instructions.md` remains the detailed operational projection. -- Expanded the retained-learning contract so `LESSONS.md` may also keep durable corrections to repeated or consequential misapplication of already-codified repository rules, then recorded the bridge-vs-projection lesson for `AGENTS.md` and `.github/copilot-instructions.md`. -- Restructured `LESSONS.md` into two tables so new or still-pending lessons stay separate from codified rules. -- Simplified `LESSONS.md` again so it now keeps only pending lessons; once a lesson is codified into a canonical owner, it is removed from the ledger instead of being duplicated there. +- Expanded the retained-learning contract so `LESSONS_LEARNED.md` may also keep durable corrections to repeated or consequential misapplication of already-codified repository rules, then recorded the bridge-vs-projection lesson for `AGENTS.md` and `.github/copilot-instructions.md`. +- Restructured `LESSONS_LEARNED.md` into two tables so new or still-pending lessons stay separate from codified rules. +- Simplified `LESSONS_LEARNED.md` again so it now keeps only pending lessons; once a lesson is codified into a canonical owner, it is removed from the ledger instead of being duplicated there. ## 2026-04-11 - Tightened the Python skill split instead of collapsing it: clarified the shared baseline in `.github/instructions/internal-python.instructions.md`, sharpened `internal-project-python` around structured package and application boundaries, and expanded `internal-script-python` plus its layout reference to cover the repository-aligned toolkit pattern used under `.github/scripts/` with shared `lib/`, hash-locked `requirements.txt`, shared `run.sh`, root-level tests, and thin wrapper entrypoints. diff --git a/.github/agents/internal-sync-global-copilot-configs-into-repo.agent.md b/.github/agents/internal-sync-global-copilot-configs-into-repo.agent.md index 8f7fe84..8c33995 100644 --- a/.github/agents/internal-sync-global-copilot-configs-into-repo.agent.md +++ b/.github/agents/internal-sync-global-copilot-configs-into-repo.agent.md @@ -1,6 +1,6 @@ --- name: internal-sync-global-copilot-configs-into-repo -description: Use this agent when aligning a consumer repository to the managed GitHub Copilot baseline from this standards repository, plus the explicitly shared repository-hygiene files declared by the paired sync skill. Keep the paired sync skill as the reusable sync-procedure owner, preserve target `local-*` extensions plus any `.github/local-copilot-overrides.md` layer, and keep root-guidance files aligned to their separate ownership layers. +description: Use this agent when aligning a consumer repository to the managed GitHub Copilot baseline from this standards repository, plus the explicitly shared repository-hygiene files and retained-learning ledger contract declared by the paired sync skill. Keep the paired sync skill as the reusable sync-procedure owner, preserve target `local-*` extensions plus any `.github/local-copilot-overrides.md` layer, and keep root-guidance files aligned to their separate ownership layers. tools: ["read", "edit", "search", "execute", "web", "agent"] agents: [] --- @@ -42,13 +42,14 @@ Treat this agent plus `.github/skills/internal-agent-sync-global-copilot-configs - Start in `plan` by default. Move to `apply` only on explicit request and only when the plan is conflict-safe. - Keep target assumptions narrow and let the paired skill own the mirrored-scope and plan-file details. - Preserve target `local-*` assets plus any consumer-owned `.github/local-copilot-overrides.md` file, exclude repository-owned `internal-sync-*` resources from mirroring, and keep root-guidance files layered according to the paired skill contract. +- When repository-root `LESSONS_LEARNED.md` is in scope, ensure the target has it and keep it structurally aligned with the source contract while preserving or migrating target-authored lesson rows through the paired skill workflow. - Sync only the managed cross-repository baseline declared by the paired skill contract; do not expand beyond that scope unless the user explicitly asks for more. - Do not restate reusable sync procedure in this agent; when the contract drifts, update the paired skill first and then realign this agent. ## Routing - Use this agent for consumer-repository baseline propagation, drift assessment, `plan`, and `apply` runs. -- Use this agent when the target must inherit the current bridge model around `AGENTS.md`, `.github/copilot-instructions.md`, `.github/local-copilot-overrides.md`, and `.github/INVENTORY.md`. +- Use this agent when the target must inherit the current bridge model around `AGENTS.md`, `.github/copilot-instructions.md`, `.github/local-copilot-overrides.md`, `.github/INVENTORY.md`, and repository-root `LESSONS_LEARNED.md`. - Do not use this agent for source-side catalog redesign, agent or skill authoring, or governance restructuring in this repository; recommend `internal-planning-leader` instead. - Do not use this agent for routine local execution once the sync contract is already settled and only a small target-local edit remains; recommend the appropriate executor for that repository instead. - When current platform behavior is the deciding factor, validate it through `internal-copilot-docs-research` before changing the sync policy. @@ -62,7 +63,7 @@ Treat this agent plus `.github/skills/internal-agent-sync-global-copilot-configs ## Output Expectations - Target analysis and selected mode -- Root-guidance alignment strategy +- Root-guidance alignment strategy and `LESSONS_LEARNED.md` sync status - Preserved `local-*` assets, `.github/local-copilot-overrides.md` status, and target-only cleanup decisions - Boundary or approval decisions that affected the selected mode - Validation results, remaining blockers, and the completion-report sections diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fd4fae9..55f680d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -82,19 +82,19 @@ You are an expert software and platform engineer. Protect correctness, security, - Also treat a repeated or consequential misapplication of an already-codified repository rule as a lesson when the correction is likely to prevent the same mistake in future work. - When a validator, IDE, schema check, or runtime error overturns an earlier assumption, immediately re-check whether that correction is durable enough to retain or codify. - Before finalizing such a correction, read the primary documentation for the relevant platform or schema instead of relying on memory or partial recall. -- Before editing repository-root `LESSONS.md`, read its current on-disk contents and treat them as the source of truth for in-progress local lessons, including uncommitted rows already present on disk. -- When a durable lesson is clear and still uncodified, append one concise, reusable row to the pending table in `LESSONS.md` instead of waiting for task completion; do not regenerate, reorder, or rewrite unrelated rows. +- Before editing repository-root `LESSONS_LEARNED.md`, read its current on-disk contents and treat them as the source of truth for in-progress local lessons, including uncommitted rows already present on disk. +- When a durable lesson is clear and still uncodified, append one concise, reusable row to the pending table in `LESSONS_LEARNED.md` instead of waiting for task completion; do not regenerate, reorder, or rewrite unrelated rows. - If you decide not to record a lesson after such a correction, make that decision explicit in the completion report with a short reason. -- Treat `LESSONS.md` as a learning ledger, not as canonical policy. Do not dump transient notes, full debugging timelines, sensitive content, or conversational noise into it. -- Preserve unrelated existing lessons in `LESSONS.md`, including local uncommitted ones already on disk. +- Treat `LESSONS_LEARNED.md` as a learning ledger, not as canonical policy. Do not dump transient notes, full debugging timelines, sensitive content, or conversational noise into it. +- Preserve unrelated existing lessons in `LESSONS_LEARNED.md`, including local uncommitted ones already on disk. - If a lesson is later disproven, narrowed, deduplicated, or codified elsewhere in the same task, update or remove only that lesson's row before completion. -- If the same task also codifies the lesson into `AGENTS.md`, this file, a scoped instruction, a skill, or an agent, remove that corresponding row from `LESSONS.md` instead of keeping a codified duplicate there. -- If no durable lesson emerged, do not force a `LESSONS.md` change. +- If the same task also codifies the lesson into `AGENTS.md`, this file, a scoped instruction, a skill, or an agent, remove that corresponding row from `LESSONS_LEARNED.md` instead of keeping a codified duplicate there. +- If no durable lesson emerged, do not force a `LESSONS_LEARNED.md` change. ## Completion Report - End completed operations with `✅ Outcome`. - When used, also include `🤖 Agents`, `📘 Instructions`, `🧩 Skills`, and `📦 Other Resources`. - In each included section, state which resources were used and why they were relevant. -- When `LESSONS.md` was updated, mention it under `📦 Other Resources` with a short reason. +- When `LESSONS_LEARNED.md` was updated, mention it under `📦 Other Resources` with a short reason. - Omit unused categories instead of adding empty or negative sections. diff --git a/.github/instructions/internal-github-actions.instructions.md b/.github/instructions/internal-github-actions.instructions.md index f3fdc65..6398df0 100644 --- a/.github/instructions/internal-github-actions.instructions.md +++ b/.github/instructions/internal-github-actions.instructions.md @@ -27,6 +27,7 @@ applyTo: "**/workflows/**,**/actions/**/action.y*ml" - Before making or validating workflow changes that depend on expression scope, context usage, or key-specific rules, read GitHub's official workflow syntax and context-availability documentation; do not rely on memory. - Do not place runner-derived paths such as `runner.temp` in workflow-root `env` or `jobs..env`; resolve them in step-level keys that allow `runner`, or derive them from default runner environment variables inside `run`. - Treat IDE, parser, `actionlint`, and queue-time errors such as `Unrecognized named-value` as mandatory documentation-check triggers. +- When debugging workflow logs, identify the first failed step before treating an earlier cache or setup line as the root cause; cache misses are informational unless the action or workflow explicitly makes them fatal. - Keep cache keys deterministic from lockfiles, tool versions, or other stable inputs instead of timestamps or branch-only entropy. - Set explicit artifact `retention-days` when artifacts bridge review, release, or deploy stages. - Validate `workflow_dispatch` free-form inputs before shell, deploy, or infrastructure steps consume them. diff --git a/.github/scripts/lib/shared.py b/.github/scripts/lib/shared.py index 646a839..56275ab 100644 --- a/.github/scripts/lib/shared.py +++ b/.github/scripts/lib/shared.py @@ -24,8 +24,10 @@ IGNORED_SYNC_FILENAMES = {"README.md", "CHANGELOG.md"} IGNORED_SYNC_PARTS = {"__pycache__", ".venv"} CONSUMER_SYNC_EXCLUDED_PREFIX = "internal-sync-" +LESSONS_PATH = "LESSONS_LEARNED.md" MANAGED_ROOT_FILES = ( "AGENTS.md", + LESSONS_PATH, ".editorconfig", ".pre-commit-config.yaml", ".github/copilot-instructions.md", @@ -86,6 +88,7 @@ class SyncPlan: operations: tuple[SyncOperation, ...] local_assets: tuple[str, ...] generated_inventory: str + generated_lessons: str | None = None generated_gitignore: str | None = None def to_dict(self) -> dict[str, object]: diff --git a/.github/scripts/lib/syncing.py b/.github/scripts/lib/syncing.py index 1d5f9d0..c999802 100644 --- a/.github/scripts/lib/syncing.py +++ b/.github/scripts/lib/syncing.py @@ -1,7 +1,10 @@ from __future__ import annotations +import hashlib import json +import re from datetime import datetime, timezone +from dataclasses import dataclass from pathlib import Path from shutil import copy2 @@ -9,6 +12,7 @@ from .fingerprinting import HASH_ALGO, NORMALIZATION_VERSION, build_fingerprint from .shared import ( INVENTORY_PATH, + LESSONS_PATH, LOCAL_COPILOT_OVERRIDES_PATH, MANAGED_ROOT_FILES, MANAGED_WORKFLOW_FILES, @@ -35,6 +39,15 @@ ) TARGET_GITIGNORE_PATH = ".gitignore" TARGET_SUPERPOWERS_IGNORE_ENTRY = "/tmp/superpowers/" +NO_PENDING_LESSONS_MARKERS = {"No pending lessons currently."} + + +@dataclass(frozen=True) +class PendingLessonsTable: + column_count: int + data_start: int + data_end: int + section_end: int def build_sync_plan(source_root: Path, target_root: Path) -> SyncPlan: @@ -45,10 +58,47 @@ def build_sync_plan(source_root: Path, target_root: Path) -> SyncPlan: target_files = discover_target_managed_files(target_root) target_excluded_files = discover_target_excluded_sync_files(target_root) operations: list[SyncOperation] = [] + generated_lessons: str | None = None for relative_path in sorted(source_files): source_path = source_root / relative_path target_path = target_root / relative_path + if relative_path == LESSONS_PATH: + generated_lessons = render_synced_lessons( + read_text(source_path), + read_text(target_path) if target_path.exists() else None, + ) + desired_hash = sha256_text(generated_lessons) + if not target_path.exists(): + operations.append( + SyncOperation( + action="create", + path=relative_path, + reason="Target learning ledger missing; create it from the source structure.", + source_hash=desired_hash, + target_hash=None, + ) + ) + continue + + target_hash = sha256_file(target_path) + action = "unchanged" if target_hash == desired_hash else "update" + reason = ( + "Target learning ledger already matches the source structure and preserved lessons." + if action == "unchanged" + else "Target learning ledger must align with the source structure while preserving target-authored lessons." + ) + operations.append( + SyncOperation( + action=action, + path=relative_path, + reason=reason, + source_hash=desired_hash, + target_hash=target_hash, + ) + ) + continue + source_hash = sha256_file(source_path) if not target_path.exists(): operations.append( @@ -192,10 +242,124 @@ def build_sync_plan(source_root: Path, target_root: Path) -> SyncPlan: operations=ordered_operations, local_assets=tuple(sorted(local_assets)), generated_inventory=generated_inventory, + generated_lessons=generated_lessons, generated_gitignore=generated_gitignore, ) +def render_synced_lessons(source_content: str, target_content: str | None) -> str: + source_lines = source_content.splitlines() + pending_table = find_pending_lessons_table(source_lines) + if pending_table is None: + return ensure_trailing_newline(source_content) + + target_rows = extract_pending_lessons_rows(target_content) + normalized_rows = [ + normalize_pending_lessons_row(row, pending_table.column_count) + for row in target_rows + ] + section_suffix = source_lines[pending_table.data_end : pending_table.section_end] + if normalized_rows: + section_suffix = [ + line + for line in section_suffix + if line.strip() not in NO_PENDING_LESSONS_MARKERS + ] + + merged_lines = ( + source_lines[: pending_table.data_start] + + [format_markdown_table_row(row) for row in normalized_rows] + + section_suffix + + source_lines[pending_table.section_end :] + ) + return ensure_trailing_newline("\n".join(merged_lines)) + + +def extract_pending_lessons_rows(content: str | None) -> list[list[str]]: + if not content: + return [] + lines = content.splitlines() + pending_table = find_pending_lessons_table(lines) + if pending_table is None: + return [] + + rows: list[list[str]] = [] + for line in lines[pending_table.data_start : pending_table.data_end]: + cells = parse_markdown_table_row(line) + if any(cell for cell in cells): + rows.append(cells) + return rows + + +def find_pending_lessons_table(lines: list[str]) -> PendingLessonsTable | None: + section_start: int | None = None + for index, line in enumerate(lines): + if line.strip() == "## Pending Rules": + section_start = index + 1 + break + if section_start is None: + return None + + section_end = len(lines) + for index in range(section_start, len(lines)): + if lines[index].startswith("## "): + section_end = index + break + + header_index: int | None = None + for index in range(section_start, section_end): + if lines[index].lstrip().startswith("|"): + header_index = index + break + if header_index is None or header_index + 1 >= section_end: + return None + if not is_markdown_table_separator(lines[header_index + 1]): + return None + + data_start = header_index + 2 + data_end = data_start + while data_end < section_end and lines[data_end].lstrip().startswith("|"): + data_end += 1 + + return PendingLessonsTable( + column_count=len(parse_markdown_table_row(lines[header_index])), + data_start=data_start, + data_end=data_end, + section_end=section_end, + ) + + +def is_markdown_table_separator(line: str) -> bool: + cells = parse_markdown_table_row(line) + return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells) + + +def parse_markdown_table_row(line: str) -> list[str]: + stripped = line.strip() + if not stripped.startswith("|"): + return [] + core = stripped.strip("|") + return [cell.strip() for cell in core.split("|")] + + +def normalize_pending_lessons_row(row: list[str], column_count: int) -> list[str]: + if len(row) >= column_count: + return row[:column_count] + return row + [""] * (column_count - len(row)) + + +def format_markdown_table_row(cells: list[str]) -> str: + return f"| {' | '.join(cells)} |" + + +def ensure_trailing_newline(content: str) -> str: + return content if content.endswith("\n") else f"{content}\n" + + +def sha256_text(content: str) -> str: + return hashlib.sha256(content.encode("utf-8")).hexdigest() + + def discover_source_sync_files(root: Path) -> set[str]: files = {relative_path for relative_path in MANAGED_ROOT_FILES if (root / relative_path).exists()} files.update(relative_path for relative_path in MANAGED_WORKFLOW_FILES if (root / relative_path).exists()) @@ -359,9 +523,14 @@ def apply_sync_plan(plan: SyncPlan, allow_dirty_target: bool = False) -> Path: for operation in plan.operations: target_path = plan.target_root / operation.path if operation.action in {"create", "update"}: - source_path = plan.source_root / operation.path target_path.parent.mkdir(parents=True, exist_ok=True) - copy2(source_path, target_path) + if operation.path == LESSONS_PATH: + if plan.generated_lessons is None: + raise RuntimeError("Generated LESSONS_LEARNED.md content missing from sync plan.") + write_text(target_path, plan.generated_lessons) + else: + source_path = plan.source_root / operation.path + copy2(source_path, target_path) elif operation.action == "delete": if target_path.exists(): target_path.unlink() diff --git a/.github/skills/internal-agent-sync-global-copilot-configs-into-repo/SKILL.md b/.github/skills/internal-agent-sync-global-copilot-configs-into-repo/SKILL.md index d407c16..8c6b459 100644 --- a/.github/skills/internal-agent-sync-global-copilot-configs-into-repo/SKILL.md +++ b/.github/skills/internal-agent-sync-global-copilot-configs-into-repo/SKILL.md @@ -1,6 +1,6 @@ --- name: internal-agent-sync-global-copilot-configs-into-repo -description: Use when aligning a consumer repository to this repository's managed GitHub Copilot baseline plus the explicitly shared repository-hygiene files, including mirror planning, apply runs, drift checks, and preservation of target `local-*` assets plus any `.github/local-copilot-overrides.md` layer. +description: Use when aligning a consumer repository to this repository's managed GitHub Copilot baseline plus the explicitly shared repository-hygiene files and retained-learning ledger template, including mirror planning, apply runs, drift checks, and preservation of target `local-*` assets plus any `.github/local-copilot-overrides.md` layer. --- # Internal Agent Sync Global Copilot Configs Into Repo @@ -16,6 +16,7 @@ The paired agent should not restate default mode handling, preserved `local-*` b - Align a consumer repository with the managed GitHub Copilot baseline from this repository. - Refresh target `AGENTS.md`, `.github/copilot-instructions.md`, and `.github/INVENTORY.md` to the current bridge 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/terraform-pre-commit.yml`. +- Refresh repository-root `LESSONS_LEARNED.md` from the source structure while preserving and, when needed, migrating target-authored pending lesson rows. - Preserve or review a target `.github/local-copilot-overrides.md` file that locally overrides the synced baseline. - Run or interpret `.github/scripts/sync_copilot_catalog.sh` or `.github/scripts/sync_copilot_catalog.py`. - Audit source-target drift before or after a sync. @@ -28,6 +29,7 @@ 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`. - Do not mirror a source `.github/local-copilot-overrides.md`; it stays consumer-owned even when the source repository has one. - Keep root guidance layered: `AGENTS.md` is the bridge, `.github/copilot-instructions.md` is the repo-wide projection, `.github/local-copilot-overrides.md` is the consumer-local exception layer, 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/`. - Prefer the bundled sync automation when it matches the requested mode instead of re-deriving the workflow manually. @@ -37,7 +39,7 @@ The paired agent should not restate default mode handling, preserved `local-*` b 1. Analyze the source baseline, target catalog, target git state, and preserved local assets. 2. Write `tmp/copilot-sync.plan.md` in the target repository with the pending operations and any manual follow-up that remains outside automation. -3. In `apply`, mirror source-managed assets, rebuild the target inventory, write the target manifest, and clear the tracking plan when nothing remains pending. +3. In `apply`, mirror source-managed assets, merge target `LESSONS_LEARNED.md` rows into the current source structure, rebuild the target inventory, write the target manifest, and clear the tracking plan when nothing remains pending. 4. Re-run the closest existing validation and report any blockers or gaps. ## Mode Selection diff --git a/.github/skills/internal-agent-sync-global-copilot-configs-into-repo/references/sync-contract.md b/.github/skills/internal-agent-sync-global-copilot-configs-into-repo/references/sync-contract.md index 889189c..950272e 100644 --- a/.github/skills/internal-agent-sync-global-copilot-configs-into-repo/references/sync-contract.md +++ b/.github/skills/internal-agent-sync-global-copilot-configs-into-repo/references/sync-contract.md @@ -4,9 +4,10 @@ Use this reference when the paired agent or this skill needs the exact sync rule ## Source-Managed Scope -Mirror these source-managed paths into the consumer repository: +Mirror or structurally align these source-managed paths into the consumer repository: - `AGENTS.md` +- `LESSONS_LEARNED.md` - `.editorconfig` - `.pre-commit-config.yaml` - `.github/copilot-instructions.md` @@ -22,6 +23,7 @@ Mirror these source-managed paths into the consumer repository: Do not sync `README.md`, changelogs, other workflows, templates, or bootstrap helpers unless the user explicitly expands scope. Do not sync consumer-facing resources whose file or directory name starts with `internal-sync-`; those remain source-only operational controls for the standards repository. +Treat `LESSONS_LEARNED.md` as a structure-managed exception: sync the source template and contract, but preserve target-authored pending lesson rows instead of copying source rows into consumer repositories. ## Target Rules @@ -30,6 +32,7 @@ Do not sync consumer-facing resources whose file or directory name starts with ` - Delete target-owned non-`local-*` assets inside mirrored categories during `apply`. - Do not mirror a source `.github/local-copilot-overrides.md` into consumer repositories. - 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. - Ensure the target root `.gitignore` contains an ignore entry for `tmp/superpowers/`. - Treat the `.gitignore` update as target-local hygiene: ensure the required ignore entry exists without mirroring the source repository `.gitignore`. @@ -38,6 +41,7 @@ Do not sync consumer-facing resources whose file or directory name starts with ` 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 +- `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 - `.github/copilot-instructions.md`: repo-wide GitHub Copilot projection - `.github/local-copilot-overrides.md`: consumer-local exception layer authorized by `AGENTS.md`; it may override synced defaults only when conflict, scope, reason, and disclosure are explicit - `.github/INVENTORY.md`: exact live catalog generated from target filesystem state @@ -80,7 +84,7 @@ If a dedicated contract test is missing, call out the gap explicitly. Completed runs should make these facts visible: - target analysis and selected mode -- root-guidance alignment strategy +- root-guidance alignment strategy and `LESSONS_LEARNED.md` status - preserved `local-*` assets and `.github/local-copilot-overrides.md` status - target-only cleanup decisions - plan-file status and lifecycle diff --git a/.github/skills/internal-github-actions/SKILL.md b/.github/skills/internal-github-actions/SKILL.md index daafe42..5cf3ecf 100644 --- a/.github/skills/internal-github-actions/SKILL.md +++ b/.github/skills/internal-github-actions/SKILL.md @@ -54,6 +54,7 @@ Use `internal-devops-core-principles` when the question is delivery strategy, re | Missing `environment` protection on production deploys | Anyone who can push to the branch can deploy to production | Add an environment with required reviewers | | Letting `workflow_dispatch` inputs flow directly into shell or deploy steps | Free-form input becomes a control path without validation | Validate and normalize inputs in an early step or job | | Using `runner.temp` or other runner-scoped contexts in workflow-root `env` or `jobs..env` | The workflow fails validation before it even queues | Use `runner` only in keys that allow it, or derive the path from runner environment variables inside `run` | +| Treating a cache miss or restore notice as the failing condition | You stop at an informational setup line and fix the wrong thing | Find the first failed step and confirm whether the action can actually fail on that message | | Using timestamp-driven or branch-only cache keys | Cache hits become noisy, stale, or misleading across runs | Key caches from lockfiles, tool versions, and other stable inputs | | Uploading artifacts without explicit name or retention | Review and deploy handoffs become ambiguous and harder to clean up | Name artifacts deliberately and set `retention-days` | | Duplicating steps across workflows instead of reusable workflow or composite action | Maintenance burden grows with every copy | Extract to a reusable workflow in one repo or a composite action across repos | @@ -79,6 +80,7 @@ Use `internal-devops-core-principles` when the question is delivery strategy, re - `actionlint` on workflow files, if available. - When expressions use non-global contexts, compare each workflow key against GitHub's official context-availability table. +- For CI-log debugging, verify the first failed step before treating earlier cache or setup lines as causal. - Verify there is no `permissions: write-all` and no missing permissions block where least privilege matters. - Verify all third-party `uses:` lines reference full SHAs instead of tags. - Re-check that every referenced local guide resolves before treating the skill update as complete. diff --git a/.github/workflows/terraform-pre-commit.yml b/.github/workflows/terraform-pre-commit.yml index c4b0ef2..e6743c9 100644 --- a/.github/workflows/terraform-pre-commit.yml +++ b/.github/workflows/terraform-pre-commit.yml @@ -50,12 +50,15 @@ jobs: persist-credentials: false - name: Restore pre-commit cache - # actions/cache@v5.0.4 + id: pre-commit-cache + # actions/cache/restore@v5.0.4 # https://github.com/actions/cache/releases/tag/v5.0.4 - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 with: path: ${{ runner.temp }}/pre-commit-cache - key: pre-commit-${{ runner.os }}-v1-${{ hashFiles('.pre-commit-config.yaml') }} + key: pre-commit-${{ runner.os }}-config-hash-${{ hashFiles(inputs.pre-commit-config || '.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}-config-hash- - name: Ensure pre-commit cache directory env: @@ -72,6 +75,7 @@ jobs: docker run --rm --entrypoint cat "${PRE_COMMIT_IMAGE}" /usr/bin/tools_versions_info - name: Run pre-commit in container + id: pre-commit-run env: PRE_COMMIT_CACHE_DIR: ${{ runner.temp }}/pre-commit-cache run: | @@ -89,3 +93,12 @@ jobs: --verbose \ --show-diff-on-failure \ --color always + + - name: Save pre-commit cache + if: ${{ always() && steps.pre-commit-run.outcome != 'skipped' && steps.pre-commit-cache.outputs.cache-hit != 'true' }} + # actions/cache/save@v5.0.4 + # https://github.com/actions/cache/releases/tag/v5.0.4 + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 + with: + path: ${{ runner.temp }}/pre-commit-cache + key: pre-commit-${{ runner.os }}-config-hash-${{ hashFiles(inputs.pre-commit-config || '.pre-commit-config.yaml') }} diff --git a/AGENTS.md b/AGENTS.md index 883dba6..333c155 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ This file is the stable entrypoint for the repository instruction architecture. - `AGENTS.md` owns repository-wide defaults, rule placement, and bridge behavior. - `.github/copilot-instructions.md` projects the repo-wide behavior that must remain visible in native Copilot flows and must stay aligned with this file. - Narrower scoped instructions may override defaults only inside their declared scope. +- Before adding a new policy, decide whether it truly belongs at repository scope; prefer the smallest specific instruction, skill, agent, or configuration that fully owns the behavior, and promote it to `AGENTS.md` only when it changes cross-surface governance or applies across the AI configuration baseline. - When rules conflict, prefer the smallest valid scope; if scope is equal, follow the canonical rule stated here and remove the conflicting duplicate. ## Language Default @@ -70,12 +71,12 @@ This file is the stable entrypoint for the repository instruction architecture. ## Retained Learning -- Root `LESSONS.md` is the repository learning ledger for durable lessons discovered during repository work, regardless of phase. +- Root `LESSONS_LEARNED.md` is the repository learning ledger for durable lessons discovered during repository work, regardless of phase. - Record or codify a durable lesson as soon as it becomes clear enough to be reusable; do not wait for task completion only because the work is still in planning, review, debugging, or implementation. - When a validator, IDE, schema check, or runtime error overturns an earlier implementation assumption, re-evaluate retained learning immediately instead of treating the correction as task-local by default. - When correctness depends on vendor-owned workflow semantics, schema constraints, or context availability, read the primary documentation before editing or asserting that a change is valid. -- Keep `LESSONS.md` non-canonical. It must not replace `AGENTS.md`, `.github/copilot-instructions.md`, scoped instructions, skills, or agents. -- Keep `LESSONS.md` append-preserving by default: preserve unrelated rows already on disk, including local uncommitted lessons, and change a specific row only when that same lesson is being codified, disproven, narrowed, or deduplicated. +- Keep `LESSONS_LEARNED.md` non-canonical. It must not replace `AGENTS.md`, `.github/copilot-instructions.md`, scoped instructions, skills, or agents. +- Keep `LESSONS_LEARNED.md` append-preserving by default: preserve unrelated rows already on disk, including local uncommitted lessons, and change a specific row only when that same lesson is being codified, disproven, narrowed, or deduplicated. - Durable corrections to repeated or consequential misapplication of existing repository rules may also be retained as lessons. - Keep detailed retained-learning behavior in `.github/copilot-instructions.md`; keep only the strategic boundary here. diff --git a/INTERNAL_CONTRACT.md b/INTERNAL_CONTRACT.md index 3ae993f..1a9e27e 100644 --- a/INTERNAL_CONTRACT.md +++ b/INTERNAL_CONTRACT.md @@ -204,13 +204,13 @@ Treat the current instruction architecture as the source of truth. Do not infer - Scope: - root `AGENTS.md` - `.github/copilot-instructions.md` - - `LESSONS.md` + - `LESSONS_LEARNED.md` - Expected behavior: - - repository-root `LESSONS.md` exists as a non-canonical learning ledger + - 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 - no ledger update is required when no stable new lesson emerged - - once a lesson is codified elsewhere, it is removed from `LESSONS.md` instead of being retained as a codified duplicate + - once a lesson is codified elsewhere, it is removed from `LESSONS_LEARNED.md` instead of being retained as a codified duplicate ### Future Automation Constraints diff --git a/LESSONS.md b/LESSONS_LEARNED.md similarity index 84% rename from LESSONS.md rename to LESSONS_LEARNED.md index fb8f5e7..c01540d 100644 --- a/LESSONS.md +++ b/LESSONS_LEARNED.md @@ -9,7 +9,9 @@ This file retains durable lessons discovered while completing tasks in this repo - Also record durable corrections to repeated or consequential misapplication of already-codified repository rules when that correction is likely to prevent future mistakes. - When a validator, IDE, schema check, or runtime error overturns an earlier assumption, re-check immediately whether the correction is durable enough to retain until it is codified or deliberately dropped. - Before deciding whether to retain, codify, or drop such a correction, read the relevant primary documentation instead of relying on memory alone. +- Prefer the smallest canonical home: if the correction belongs in a scoped instruction, skill, agent, or repository config and is being codified there, do not retain a duplicate lesson row. - Keep only stable, reusable, repository-relevant lessons. +- Do not retain incident-specific or implementation-specific fixes that are too narrow to reuse beyond the triggering task or log. - Exclude secrets, transient debugging notes, raw conversation logs, and task-local noise. - Keep new or still-uncodified lessons in the pending table until they are codified or deliberately dropped. - Add a new lesson by appending one new row to the pending table; do not regenerate, reorder, or rewrite unrelated rows. diff --git a/tests/test_retained_learning_contract.py b/tests/test_retained_learning_contract.py index 83974d6..940e4c8 100644 --- a/tests/test_retained_learning_contract.py +++ b/tests/test_retained_learning_contract.py @@ -12,7 +12,7 @@ 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": [ - "Keep `LESSONS.md` append-preserving by default", + "Keep `LESSONS_LEARNED.md` append-preserving by default", "preserve unrelated rows already on disk, including local uncommitted lessons", ( "change a specific row only when that same lesson is being codified, " @@ -20,17 +20,17 @@ def test_retained_learning_contract_preserves_on_disk_lessons_rows() -> None: ), ], ".github/copilot-instructions.md": [ - "Before editing repository-root `LESSONS.md`, read its current on-disk contents", + "Before editing repository-root `LESSONS_LEARNED.md`, read its current on-disk contents", "including uncommitted rows already present on disk", - "append one concise, reusable row to the pending table in `LESSONS.md`", + "append one concise, reusable row to the pending table in `LESSONS_LEARNED.md`", "do not regenerate, reorder, or rewrite unrelated rows", ( - "Preserve unrelated existing lessons in `LESSONS.md`, including local " + "Preserve unrelated existing lessons in `LESSONS_LEARNED.md`, including local " "uncommitted ones already on disk" ), "update or remove only that lesson's row before completion", ], - "LESSONS.md": [ + "LESSONS_LEARNED.md": [ "Before editing this file, read its current on-disk contents", "including local uncommitted rows already present on disk", ( diff --git a/tests/test_sync_and_token_risks.py b/tests/test_sync_and_token_risks.py index 4b77b92..72e0b8e 100644 --- a/tests/test_sync_and_token_risks.py +++ b/tests/test_sync_and_token_risks.py @@ -175,6 +175,88 @@ def test_build_sync_plan_accepts_existing_tmp_superpowers_gitignore_entry( assert plan.generated_gitignore == "node_modules/\ntmp/superpowers/\n" +def test_apply_sync_plan_creates_target_lessons_from_source_template( + tmp_path: Path, +) -> None: + source_root = tmp_path / "source" + target_root = tmp_path / "target" + source_lessons = ( + "# Lessons\n\n" + "Source-managed retained learning ledger.\n\n" + "## Pending Rules\n\n" + "| Date | Lesson | Status | Intended canonical target |\n" + "| --- | --- | --- | --- |\n\n" + "No pending lessons currently.\n" + ) + + write_file(source_root / "AGENTS.md", "# AGENTS\nsource\n") + write_file(source_root / ".github/copilot-instructions.md", "# Copilot\nsource\n") + write_file(source_root / "LESSONS_LEARNED.md", source_lessons) + write_file(target_root / "AGENTS.md", "# AGENTS\ntarget\n") + write_file(target_root / ".github/copilot-instructions.md", "# Copilot\ntarget\n") + + plan = build_sync_plan(source_root, target_root) + actions = {(operation.action, operation.path) for operation in plan.operations} + + assert ("create", "LESSONS_LEARNED.md") in actions + + apply_sync_plan(plan) + + assert (target_root / "LESSONS_LEARNED.md").read_text( + encoding="utf-8" + ) == source_lessons + + +def test_apply_sync_plan_realigns_lessons_structure_without_losing_target_rows( + tmp_path: Path, +) -> None: + source_root = tmp_path / "source" + target_root = tmp_path / "target" + source_lessons = ( + "# Lessons\n\n" + "Source-managed retained learning ledger.\n\n" + "## Entry Rules\n\n" + "- Use the source structure.\n\n" + "## Pending Rules\n\n" + "| Date | Lesson | Status | Intended canonical target | Notes |\n" + "| --- | --- | --- | --- | --- |\n\n" + "No pending lessons currently.\n" + ) + target_lessons = ( + "# Lessons\n\n" + "Target-only intro that should be replaced.\n\n" + "## Pending Rules\n\n" + "| Date | Lesson | Status | Intended canonical target |\n" + "| --- | --- | --- | --- |\n" + "| 2026-04-12 | Preserve local lesson | pending | AGENTS.md |\n" + ) + + write_file(source_root / "AGENTS.md", "# AGENTS\nsource\n") + write_file(source_root / ".github/copilot-instructions.md", "# Copilot\nsource\n") + write_file(source_root / "LESSONS_LEARNED.md", source_lessons) + write_file(target_root / "AGENTS.md", "# AGENTS\ntarget\n") + write_file(target_root / ".github/copilot-instructions.md", "# Copilot\ntarget\n") + write_file(target_root / "LESSONS_LEARNED.md", target_lessons) + + plan = build_sync_plan(source_root, target_root) + actions = {(operation.action, operation.path) for operation in plan.operations} + + assert ("update", "LESSONS_LEARNED.md") in actions + + apply_sync_plan(plan) + + assert (target_root / "LESSONS_LEARNED.md").read_text(encoding="utf-8") == ( + "# Lessons\n\n" + "Source-managed retained learning ledger.\n\n" + "## Entry Rules\n\n" + "- Use the source structure.\n\n" + "## Pending Rules\n\n" + "| Date | Lesson | Status | Intended canonical target | Notes |\n" + "| --- | --- | --- | --- | --- |\n" + "| 2026-04-12 | Preserve local lesson | pending | AGENTS.md | |\n" + ) + + def test_build_sync_plan_includes_shared_repo_hygiene_files_only( tmp_path: Path, ) -> None: