From c64b73221c443fdceadbb42be73e649b5a2b5f49 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 19:29:00 +0100 Subject: [PATCH 01/18] Add initial version of the `move-files` skill --- .agents/skills/move-files/SKILL.md | 81 ++++++++++++++++++++ .agents/skills/move-files/agents/openai.yaml | 4 + 2 files changed, 85 insertions(+) create mode 100644 .agents/skills/move-files/SKILL.md create mode 100644 .agents/skills/move-files/agents/openai.yaml diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md new file mode 100644 index 00000000..601eded2 --- /dev/null +++ b/.agents/skills/move-files/SKILL.md @@ -0,0 +1,81 @@ +--- +name: move-files +description: > + Move, rename, or reorganize files and directories safely in a repository. + Use when an agent is asked to relocate source files, tests, documentation, + assets, modules, packages, or folders; preserve git history with `git mv` + where appropriate; update imports, package declarations, build metadata, + documentation links, and path references; and verify no stale paths remain. +--- + +# Move Files + +## Workflow + +Use this skill to move files without losing history, overwriting user work, or +leaving broken references behind. + +1. Understand the requested move. + - Identify each source path and destination path. + - Identify whether the task is a simple move, a rename, a package/module + relocation, or a directory reorganization. + - If multiple files are moving, write down the source-to-destination mapping + before editing. + +2. Inspect the current tree. + - Run `git status --short` first and treat existing changes as user-owned. + - Confirm each source exists and whether it is tracked. + - Confirm each destination does not already exist unless the user explicitly + requested a merge or overwrite. + - Use `rg --files` or targeted directory listings to understand neighboring + naming and placement conventions. + +3. Find references before moving. + - Search for old paths, filenames, package names, module names, resource + paths, and documentation links with `rg`. + - Check build files such as `settings.gradle.kts`, `build.gradle.kts`, and + `buildSrc` when moving modules, source sets, generated fixtures, or tests. + - For Kotlin and Java files, check package declarations and imports. Update + package declarations only when the move changes the intended package. + +4. Move the files. + - Prefer `git mv` for tracked files inside the same repository. + - Use normal filesystem moves only for untracked files, generated artifacts, + or moves outside git control. + - Create destination parent directories before moving. + - For case-only renames on case-insensitive filesystems, move through a + temporary intermediate name. + - Do not overwrite, delete, or merge existing destination content unless the + user explicitly asked for that exact operation. + +5. Repair references. + - Update imports, package declarations, build includes, docs links, resource + paths, test fixtures, sample paths, and scripts that mention the old path. + - Prefer precise edits over broad replacements. Inspect each search hit when + the old name is generic. + - Re-run `rg` for old paths and names until only intentional references + remain. + +6. Verify the result. + - Run `git status --short` and confirm the changed files match the requested + move. + - Run focused tests, compilation, documentation checks, or repository build + tasks appropriate to the files moved. + - If validation cannot be run, state exactly what was not run and why. + +## Repo-Specific Notes + +- Follow `.agents/project-structure-expectations.md` when moving project + modules, source sets, tests, or Gradle files. +- Follow `.agents/safety-rules.md` and never revert unrelated user changes. +- Prefer Kotlin source under `src/main/kotlin` and tests under + `src/test/kotlin` unless the surrounding module uses a different convention. + +## Final Response + +Report: + +- What moved from where to where. +- What references or metadata were updated. +- What verification ran and the result. +- Any intentional stale references or follow-up risks. diff --git a/.agents/skills/move-files/agents/openai.yaml b/.agents/skills/move-files/agents/openai.yaml new file mode 100644 index 00000000..ba90a9f8 --- /dev/null +++ b/.agents/skills/move-files/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Move Files" + short_description: "Move files safely across a repo" + default_prompt: "Use $move-files to relocate files or directories in this repository while preserving history, updating references, and verifying the result." From 60b8d4003161c72d18667d969ed663e904f91a34 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 19:36:58 +0100 Subject: [PATCH 02/18] Classify move operation complexity --- .agents/skills/move-files/SKILL.md | 119 ++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 36 deletions(-) diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md index 601eded2..c81b74ce 100644 --- a/.agents/skills/move-files/SKILL.md +++ b/.agents/skills/move-files/SKILL.md @@ -10,59 +10,106 @@ description: > # Move Files +## Complexity Classifier + +Classify the request before doing broad inspection: + +- `SIMPLE`: one or a few file moves/renames within the same module/package + intent, no destination conflicts, no module/build layout changes. +- `STRUCTURAL`: package relocations, source-set reshuffles, many-file + reorganizations, module moves, or cross-module moves. +- `RISKY`: destination exists, merge/overwrite ambiguity, case-only rename on + case-insensitive filesystems, incomplete mapping, or unclear user intent. + +Use this class to choose the workflow depth and token budget. + ## Workflow Use this skill to move files without losing history, overwriting user work, or leaving broken references behind. -1. Understand the requested move. - - Identify each source path and destination path. - - Identify whether the task is a simple move, a rename, a package/module - relocation, or a directory reorganization. - - If multiple files are moving, write down the source-to-destination mapping - before editing. +### Fast Path (`SIMPLE`) -2. Inspect the current tree. +1. Preflight (minimal). + - Identify source-to-destination mapping. - Run `git status --short` first and treat existing changes as user-owned. - - Confirm each source exists and whether it is tracked. - - Confirm each destination does not already exist unless the user explicitly - requested a merge or overwrite. - - Use `rg --files` or targeted directory listings to understand neighboring - naming and placement conventions. + - Confirm each source exists. + - Confirm destination does not exist. +2. Move. + - Prefer `git mv` for tracked files inside the same repository. + - Use filesystem moves only for untracked files or outside git control. +3. Repair only affected references. + - Run targeted searches for exact old path/name/package tokens. + - Update only concrete hits in nearby module/files first. +4. Verify by delta. + - Re-run targeted checks for old tokens. + - Run `git status --short` and confirm expected move-only delta. + +### Full Path (`STRUCTURAL` and `RISKY`) +1. Understand and map. + - Identify each source path and destination path. + - Determine move type: rename, package/module relocation, or reorganization. + - For multiple files, write down the source-to-destination mapping before + editing. +2. Inspect current tree and boundaries. + - Run `git status --short` first and treat existing changes as user-owned. + - Confirm each source exists and whether it is tracked. + - Confirm each destination does not already exist unless explicitly approved. + - Inspect neighboring naming and placement conventions. 3. Find references before moving. - Search for old paths, filenames, package names, module names, resource - paths, and documentation links with `rg`. + paths, and docs links. - Check build files such as `settings.gradle.kts`, `build.gradle.kts`, and - `buildSrc` when moving modules, source sets, generated fixtures, or tests. - - For Kotlin and Java files, check package declarations and imports. Update - package declarations only when the move changes the intended package. - -4. Move the files. + `buildSrc` for module/source-set/test moves. + - For Kotlin and Java, update package declarations only when intended package + changes. +4. Move files safely. - Prefer `git mv` for tracked files inside the same repository. - - Use normal filesystem moves only for untracked files, generated artifacts, - or moves outside git control. + - Use filesystem moves only for untracked files, generated artifacts, or + outside git control. - Create destination parent directories before moving. - - For case-only renames on case-insensitive filesystems, move through a - temporary intermediate name. - - Do not overwrite, delete, or merge existing destination content unless the - user explicitly asked for that exact operation. - -5. Repair references. + - For case-only renames on case-insensitive filesystems, move via temporary + intermediate name. + - Do not overwrite/delete/merge destination content unless explicitly asked. +5. Repair references comprehensively. - Update imports, package declarations, build includes, docs links, resource - paths, test fixtures, sample paths, and scripts that mention the old path. - - Prefer precise edits over broad replacements. Inspect each search hit when - the old name is generic. - - Re-run `rg` for old paths and names until only intentional references - remain. - -6. Verify the result. - - Run `git status --short` and confirm the changed files match the requested + paths, test fixtures, sample paths, and scripts mentioning old paths. + - Prefer precise edits over broad replacements. +6. Verify result. + - Run `git status --short` and confirm changed files match the requested move. - - Run focused tests, compilation, documentation checks, or repository build - tasks appropriate to the files moved. + - Run focused validation relevant to moved files. - If validation cannot be run, state exactly what was not run and why. +## Escalation Rules + +- Start reference search in the smallest relevant scope: + affected directory -> affected module -> repository-wide. +- Escalate only if lower-scope checks leave unresolved references. +- Stop and ask the user before continuing when: + - destination already exists and merge/overwrite intent is unclear, + - source-to-destination mapping is incomplete/ambiguous, + - move implies semantic package/module intent not specified by user. +- If uncertainty remains after escalation, report uncertainty explicitly and + avoid destructive actions. + +## Token Budget Guidance + +- Prefer targeted checks over broad repository scans. +- Use exact tokens (full old path/name/package) before fuzzy searches. +- Avoid repeating the same search once checks are clean. +- Keep intermediate status concise unless user requests detail. + +## Compact Result Schema + +Report with compact sections: + +- `Moved[]`: `source -> destination` +- `UpdatedRefs[]`: key references/metadata changed +- `Verification[]`: checks run and outcomes +- `Risks[]`: intentional stale refs, unresolved ambiguity, or follow-ups + ## Repo-Specific Notes - Follow `.agents/project-structure-expectations.md` when moving project From 8226a706400e871f285a39148d9d441509650673 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 19:42:46 +0100 Subject: [PATCH 03/18] Make the `move-files` skill more condense --- .agents/skills/move-files/SKILL.md | 149 ++++++++--------------------- 1 file changed, 38 insertions(+), 111 deletions(-) diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md index c81b74ce..1b908689 100644 --- a/.agents/skills/move-files/SKILL.md +++ b/.agents/skills/move-files/SKILL.md @@ -1,128 +1,55 @@ --- name: move-files description: > - Move, rename, or reorganize files and directories safely in a repository. - Use when an agent is asked to relocate source files, tests, documentation, - assets, modules, packages, or folders; preserve git history with `git mv` - where appropriate; update imports, package declarations, build metadata, - documentation links, and path references; and verify no stale paths remain. + Move or rename files/directories safely in a repo. Use for relocating source, + test, doc, asset, module, package, or folder paths; preserve history, update + references/build metadata, and verify stale paths are gone. --- # Move Files -## Complexity Classifier - -Classify the request before doing broad inspection: - -- `SIMPLE`: one or a few file moves/renames within the same module/package - intent, no destination conflicts, no module/build layout changes. -- `STRUCTURAL`: package relocations, source-set reshuffles, many-file - reorganizations, module moves, or cross-module moves. -- `RISKY`: destination exists, merge/overwrite ambiguity, case-only rename on - case-insensitive filesystems, incomplete mapping, or unclear user intent. - -Use this class to choose the workflow depth and token budget. - ## Workflow -Use this skill to move files without losing history, overwriting user work, or -leaving broken references behind. - -### Fast Path (`SIMPLE`) - -1. Preflight (minimal). - - Identify source-to-destination mapping. - - Run `git status --short` first and treat existing changes as user-owned. - - Confirm each source exists. - - Confirm destination does not exist. -2. Move. - - Prefer `git mv` for tracked files inside the same repository. - - Use filesystem moves only for untracked files or outside git control. -3. Repair only affected references. - - Run targeted searches for exact old path/name/package tokens. - - Update only concrete hits in nearby module/files first. -4. Verify by delta. - - Re-run targeted checks for old tokens. - - Run `git status --short` and confirm expected move-only delta. - -### Full Path (`STRUCTURAL` and `RISKY`) - -1. Understand and map. - - Identify each source path and destination path. - - Determine move type: rename, package/module relocation, or reorganization. - - For multiple files, write down the source-to-destination mapping before - editing. -2. Inspect current tree and boundaries. - - Run `git status --short` first and treat existing changes as user-owned. - - Confirm each source exists and whether it is tracked. - - Confirm each destination does not already exist unless explicitly approved. - - Inspect neighboring naming and placement conventions. -3. Find references before moving. - - Search for old paths, filenames, package names, module names, resource - paths, and docs links. - - Check build files such as `settings.gradle.kts`, `build.gradle.kts`, and - `buildSrc` for module/source-set/test moves. - - For Kotlin and Java, update package declarations only when intended package +1. Preflight. + - Run `git status --short`; treat existing changes as user-owned. + - Map each `source -> destination`. + - Confirm sources exist and destinations do not, unless merge/overwrite was + explicit. + - Classify scope: simple same-module moves stay targeted; package, module, or + cross-module moves need broader inspection. + +2. Search before moving. + - Search exact old paths, filenames, package/module names, resource paths, and + docs links. + - For Gradle/module/source-set moves, check `settings.gradle.kts`, + `build.gradle.kts`, and `buildSrc`. + - For Kotlin/Java, update package declarations only when package intent changes. -4. Move files safely. - - Prefer `git mv` for tracked files inside the same repository. - - Use filesystem moves only for untracked files, generated artifacts, or - outside git control. - - Create destination parent directories before moving. - - For case-only renames on case-insensitive filesystems, move via temporary - intermediate name. - - Do not overwrite/delete/merge destination content unless explicitly asked. -5. Repair references comprehensively. - - Update imports, package declarations, build includes, docs links, resource - paths, test fixtures, sample paths, and scripts mentioning old paths. - - Prefer precise edits over broad replacements. -6. Verify result. - - Run `git status --short` and confirm changed files match the requested - move. - - Run focused validation relevant to moved files. - - If validation cannot be run, state exactly what was not run and why. - -## Escalation Rules - -- Start reference search in the smallest relevant scope: - affected directory -> affected module -> repository-wide. -- Escalate only if lower-scope checks leave unresolved references. -- Stop and ask the user before continuing when: - - destination already exists and merge/overwrite intent is unclear, - - source-to-destination mapping is incomplete/ambiguous, - - move implies semantic package/module intent not specified by user. -- If uncertainty remains after escalation, report uncertainty explicitly and - avoid destructive actions. - -## Token Budget Guidance - -- Prefer targeted checks over broad repository scans. -- Use exact tokens (full old path/name/package) before fuzzy searches. -- Avoid repeating the same search once checks are clean. -- Keep intermediate status concise unless user requests detail. - -## Compact Result Schema -Report with compact sections: +3. Move safely. + - Prefer `git mv` for tracked files in the repo. + - Use filesystem moves only for untracked/generated/out-of-git files. + - Create parent directories first. + - For case-only renames, move through a temporary name. + - Ask before ambiguous mappings, destination conflicts, or unclear semantic + package/module changes. -- `Moved[]`: `source -> destination` -- `UpdatedRefs[]`: key references/metadata changed -- `Verification[]`: checks run and outcomes -- `Risks[]`: intentional stale refs, unresolved ambiguity, or follow-ups +4. Repair references. + - Update imports, package declarations, build metadata, docs links, resource + paths, fixtures, samples, and scripts. + - Start search scope narrow: affected directory, then module, then repo-wide. + - Prefer precise edits; avoid broad replacements on generic names. -## Repo-Specific Notes +5. Verify. + - Re-run targeted searches for old tokens. + - Run `git status --short` and confirm the delta matches the move. + - Run focused validation for moved files, or state what could not run. -- Follow `.agents/project-structure-expectations.md` when moving project - modules, source sets, tests, or Gradle files. -- Follow `.agents/safety-rules.md` and never revert unrelated user changes. -- Prefer Kotlin source under `src/main/kotlin` and tests under - `src/test/kotlin` unless the surrounding module uses a different convention. +## Repo Notes -## Final Response +Follow `.agents/project-structure-expectations.md` for module/source-set/test +moves. Prefer `src/main/kotlin` and `src/test/kotlin` unless the module differs. -Report: +## Report -- What moved from where to where. -- What references or metadata were updated. -- What verification ran and the result. -- Any intentional stale references or follow-up risks. +Return: `Moved[]`, `UpdatedRefs[]`, `Verification[]`, `Risks[]`. From 497dab5349b2118283542d5a63d7ca985835e978 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 20:03:56 +0100 Subject: [PATCH 04/18] Link `.agents/skills` to `.claude/skills` --- .claude/skills/move-files | 1 + .claude/skills/writer | 1 + 2 files changed, 2 insertions(+) create mode 120000 .claude/skills/move-files create mode 120000 .claude/skills/writer diff --git a/.claude/skills/move-files b/.claude/skills/move-files new file mode 120000 index 00000000..bfab80b9 --- /dev/null +++ b/.claude/skills/move-files @@ -0,0 +1 @@ +../../.agents/skills/move-files \ No newline at end of file diff --git a/.claude/skills/writer b/.claude/skills/writer new file mode 120000 index 00000000..cc371f0d --- /dev/null +++ b/.claude/skills/writer @@ -0,0 +1 @@ +../../.agents/skills/writer \ No newline at end of file From c5db49cd70ca55e31d58822931fb8d0b04c3baa1 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 20:11:50 +0100 Subject: [PATCH 05/18] Link `.claude/skills` to `.agents/skills` --- .claude/skills | 1 + .claude/skills/move-files | 1 - .claude/skills/writer | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) create mode 120000 .claude/skills delete mode 120000 .claude/skills/move-files delete mode 120000 .claude/skills/writer diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.claude/skills/move-files b/.claude/skills/move-files deleted file mode 120000 index bfab80b9..00000000 --- a/.claude/skills/move-files +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/move-files \ No newline at end of file diff --git a/.claude/skills/writer b/.claude/skills/writer deleted file mode 120000 index cc371f0d..00000000 --- a/.claude/skills/writer +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/writer \ No newline at end of file From 169e683f1066a28dde5a78dfec09e6ff67bcfb9a Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 20:20:56 +0100 Subject: [PATCH 06/18] Update `.claude` directory during migration --- migrate | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/migrate b/migrate index 0f914775..bc7b908b 100755 --- a/migrate +++ b/migrate @@ -30,6 +30,12 @@ if [ -d ".agents" ]; then cp -R .agents .. fi +echo "Updating \\\`.claude\\\` directory" +rm -rf ../.claude +if [ -d ".claude" ]; then + cp -R .claude .. +fi + # Copies the file or directory passed as the first parameter to the upper directory, # only if such a file or directory does not exist there. function initialize() { From 8bee3246f19000b1873ba1d86da77860f9f1fd5d Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 20:46:45 +0100 Subject: [PATCH 07/18] Draft `update-copyright` skill --- .agents/skills/update-copyright/SKILL.md | 66 ++++ .../update-copyright/agents/openai.yaml | 4 + .../scripts/update_copyright.py | 371 ++++++++++++++++++ 3 files changed, 441 insertions(+) create mode 100644 .agents/skills/update-copyright/SKILL.md create mode 100644 .agents/skills/update-copyright/agents/openai.yaml create mode 100755 .agents/skills/update-copyright/scripts/update_copyright.py diff --git a/.agents/skills/update-copyright/SKILL.md b/.agents/skills/update-copyright/SKILL.md new file mode 100644 index 00000000..8b1984f0 --- /dev/null +++ b/.agents/skills/update-copyright/SKILL.md @@ -0,0 +1,66 @@ +--- +name: update-copyright +description: > + Update source file copyright headers from the IntelliJ IDEA copyright profile + in `.idea/copyright/profiles_settings.xml`, resolving the default profile XML, + reading its `notice` option, and replacing `today.year` with the current year. + Automatically apply this skill when source files are modified in a change set. +--- + +# Copyright Update + +Use this skill when asked to add, refresh, or normalize source file copyright +statements according to the repository's IntelliJ IDEA copyright profile. + +## Workflow + +1. Preflight. + - Run `git status --short`; treat existing changes as user-owned. + - Identify the requested scope: explicit files, directories, or all tracked + source files when no scope is given. + +2. Resolve the notice text. + - Read `.idea/copyright/profiles_settings.xml`. + - In the `settings` tag, read the `default` attribute. + - Convert that profile name to the XML filename by replacing each run of + non-alphanumeric characters with `_`, trimming leading/trailing `_`, and + appending `.xml`. + Example: `TeamDev Open-Source` becomes `TeamDev_Open_Source.xml`. + - Read `.idea/copyright/`. + - Use the `value` attribute of the `option` tag whose `name` is `notice`. + - Decode XML/HTML character references, then replace `${today.year}`, + `$today.year`, or bare `today.year` with the current year. + +3. Update files. + - Prefer running `scripts/update_copyright.py`; it resolves the notice, + detects common source comment styles, replaces an existing top copyright + block when present, and inserts one when missing. + - Pass explicit paths when the user gave a scope: + + ```bash + python3 .agents/skills/update-copyright/scripts/update_copyright.py path/to/File.kt scripts/ + ``` + + - With no paths, the script updates tracked source-like files, excluding + generated/build/tooling directories and Gradle wrapper files: + + ```bash + python3 .agents/skills/update-copyright/scripts/update_copyright.py + ``` + + - Use `--dry-run` first for broad changes: + + ```bash + python3 .agents/skills/update-copyright/scripts/update_copyright.py --dry-run + ``` + +4. Verify. + - Re-run `git status --short` and inspect the changed files. + - For broad updates, spot-check at least one block-comment file, one + hash-comment file, and one XML file if present. + - Run a targeted search such as `rg -n "Copyright [0-9]{4}, TeamDev"` when + the profile follows the TeamDev notice format. + +## Report + +Return: `NoticeSource[]`, `UpdatedFiles[]`, `Verification[]`, `Risks[]`. diff --git a/.agents/skills/update-copyright/agents/openai.yaml b/.agents/skills/update-copyright/agents/openai.yaml new file mode 100644 index 00000000..246dd647 --- /dev/null +++ b/.agents/skills/update-copyright/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Copyright Update" + short_description: "Refresh source copyright headers" + default_prompt: "Use $update-copyright to refresh source file copyright headers from the IntelliJ IDEA copyright profile in this repository." diff --git a/.agents/skills/update-copyright/scripts/update_copyright.py b/.agents/skills/update-copyright/scripts/update_copyright.py new file mode 100755 index 00000000..35496b72 --- /dev/null +++ b/.agents/skills/update-copyright/scripts/update_copyright.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +"""Update source copyright headers from IntelliJ IDEA copyright profiles.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import html +import re +import subprocess +import sys +from pathlib import Path +from xml.etree import ElementTree as ET + + +BLOCK_EXTENSIONS = { + ".c", + ".cc", + ".cpp", + ".cs", + ".css", + ".cxx", + ".dart", + ".go", + ".gradle", + ".groovy", + ".h", + ".hh", + ".hpp", + ".java", + ".js", + ".jsx", + ".kt", + ".kts", + ".less", + ".m", + ".mm", + ".proto", + ".rs", + ".scala", + ".scss", + ".swift", + ".ts", + ".tsx", +} +HASH_EXTENSIONS = { + ".bash", + ".bzl", + ".properties", + ".pl", + ".py", + ".rb", + ".sh", + ".toml", + ".yaml", + ".yml", + ".zsh", +} +XML_EXTENSIONS = { + ".fxml", + ".pom", + ".wsdl", + ".xml", + ".xsd", + ".xsl", + ".xslt", +} +EXCLUDED_DIRS = { + ".agents", + ".git", + ".gradle", + ".idea", + ".kotlin", + "build", + "generated", + "out", + "tmp", +} +EXCLUDED_FILES = { + "gradlew", + "gradlew.bat", +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Update source copyright headers from " + ".idea/copyright/profiles_settings.xml." + ) + ) + parser.add_argument( + "paths", + nargs="*", + help="Files or directories to update. Defaults to tracked source files.", + ) + parser.add_argument( + "--root", + type=Path, + default=Path.cwd(), + help="Repository root. Defaults to the current working directory.", + ) + parser.add_argument( + "--year", + default=str(dt.date.today().year), + help="Year to substitute for today.year. Defaults to the current year.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Report files that would change without writing them.", + ) + parser.add_argument( + "--check", + action="store_true", + help="Exit with status 1 if any file would change; do not write files.", + ) + return parser.parse_args() + + +def profile_filename(profile_name: str) -> str: + stem = re.sub(r"[^A-Za-z0-9]+", "_", profile_name).strip("_") + if not stem: + raise ValueError("The default copyright profile name is empty.") + return f"{stem}.xml" + + +def load_notice(root: Path, year: str) -> tuple[str, Path]: + settings_path = root / ".idea" / "copyright" / "profiles_settings.xml" + if not settings_path.is_file(): + raise FileNotFoundError(f"Missing {settings_path}") + + settings_root = ET.parse(settings_path).getroot() + settings = settings_root.find(".//settings") + if settings is None: + raise ValueError(f"{settings_path} does not contain a settings tag.") + + default_profile = settings.get("default") + if not default_profile: + raise ValueError(f"{settings_path} settings tag has no default attribute.") + + profile_path = settings_path.parent / profile_filename(default_profile) + if not profile_path.is_file(): + raise FileNotFoundError( + f"Default profile {default_profile!r} resolves to missing {profile_path}" + ) + + profile_root = ET.parse(profile_path).getroot() + notice = None + for option in profile_root.findall(".//option"): + if option.get("name") == "notice": + notice = option.get("value") + break + if notice is None: + raise ValueError(f"{profile_path} has no option named 'notice'.") + + decoded = html.unescape(notice) + decoded = decoded.replace("${today.year}", year) + decoded = decoded.replace("$today.year", year) + decoded = decoded.replace("today.year", year) + return decoded.rstrip(), profile_path + + +def style_for(path: Path) -> str | None: + name = path.name + suffix = path.suffix.lower() + if name.endswith((".sh.template", ".bash.template", ".zsh.template")): + return "hash" + if suffix in BLOCK_EXTENSIONS: + return "block" + if suffix in HASH_EXTENSIONS: + return "hash" + if suffix in XML_EXTENSIONS: + return "xml" + return None + + +def is_excluded(path: Path) -> bool: + if path.name in EXCLUDED_FILES: + return True + parts = path.parts + if len(parts) >= 2 and parts[0] == "gradle" and parts[1] == "wrapper": + return True + return any(part in EXCLUDED_DIRS for part in parts) + + +def tracked_files(root: Path) -> list[Path]: + try: + result = subprocess.run( + ["git", "-C", str(root), "ls-files", "-z"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except (FileNotFoundError, subprocess.CalledProcessError): + return [ + path.relative_to(root) + for path in root.rglob("*") + if path.is_file() and not is_excluded(path.relative_to(root)) + ] + + return [ + Path(item) + for item in result.stdout.decode("utf-8").split("\0") + if item + ] + + +def expand_requested_paths(root: Path, requested: list[str]) -> list[Path]: + if not requested: + paths = tracked_files(root) + else: + paths = [] + for item in requested: + path = (root / item).resolve() + if not path.exists(): + raise FileNotFoundError(f"Path does not exist: {item}") + if path.is_dir(): + for child in path.rglob("*"): + if child.is_file(): + paths.append(child.relative_to(root)) + else: + paths.append(path.relative_to(root)) + + unique = sorted(set(paths), key=lambda p: p.as_posix()) + return [ + path + for path in unique + if style_for(path) is not None and not is_excluded(path) + ] + + +def newline_for(text: str) -> str: + return "\r\n" if "\r\n" in text else "\n" + + +def build_header(notice: str, style: str, newline: str) -> str: + lines = notice.splitlines() + if style == "block": + body = newline.join(f" * {line}" if line else " *" for line in lines) + return f"/*{newline}{body}{newline} */{newline}{newline}" + if style == "hash": + body = newline.join(f"# {line}" if line else "#" for line in lines) + return f"{body}{newline}{newline}" + if style == "xml": + body = newline.join(f" ~ {line}" if line else " ~" for line in lines) + return f"{newline}{newline}" + raise ValueError(f"Unsupported comment style: {style}") + + +def split_leading_directive(text: str, style: str, newline: str) -> tuple[str, str]: + if style == "hash" and text.startswith("#!"): + line_end = text.find("\n") + if line_end == -1: + return text + newline + newline, "" + prefix = text[: line_end + 1] + newline + return prefix, strip_leading_blank_lines(text[line_end + 1 :]) + + if style == "xml" and text.startswith("") + if close != -1: + line_end = text.find("\n", close) + if line_end == -1: + return text + newline + newline, "" + prefix = text[: line_end + 1] + newline + return prefix, strip_leading_blank_lines(text[line_end + 1 :]) + + return "", strip_leading_blank_lines(text) + + +def strip_leading_blank_lines(text: str) -> str: + return re.sub(r"^(?:[ \t]*\r?\n)+", "", text) + + +def strip_existing_header(text: str, style: str) -> str: + if style == "block" and text.startswith("/*"): + close = text.find("*/") + if close != -1: + candidate = text[: close + 2] + if is_copyright_header(candidate): + return strip_leading_blank_lines(text[close + 2 :]) + + if style == "xml" and text.startswith("") + if close != -1: + candidate = text[: close + 3] + if is_copyright_header(candidate): + return strip_leading_blank_lines(text[close + 3 :]) + + if style == "hash": + lines = text.splitlines(keepends=True) + end = 0 + for line in lines: + stripped = line.strip() + if stripped == "" or stripped.startswith("#"): + end += len(line) + continue + break + candidate = text[:end] + if candidate and is_copyright_header(candidate): + return strip_leading_blank_lines(text[end:]) + + return text + + +def is_copyright_header(text: str) -> bool: + limited = text[:5000] + return "Copyright" in limited and ( + "Licensed under" in limited or "All rights reserved" in limited + ) + + +def updated_text(text: str, notice: str, style: str) -> str: + bom = "\ufeff" if text.startswith("\ufeff") else "" + if bom: + text = text[1:] + newline = newline_for(text) + prefix, body = split_leading_directive(text, style, newline) + body = strip_existing_header(body, style) + return bom + prefix + build_header(notice, style, newline) + body + + +def update_file(root: Path, path: Path, notice: str, dry_run: bool) -> bool: + absolute = root / path + style = style_for(path) + if style is None: + return False + + try: + text = absolute.read_text(encoding="utf-8") + except UnicodeDecodeError: + print(f"Skipping non-UTF-8 file: {path}", file=sys.stderr) + return False + + next_text = updated_text(text, notice, style) + if next_text == text: + return False + + if not dry_run: + with absolute.open("w", encoding="utf-8", newline="") as file: + file.write(next_text) + return True + + +def main() -> int: + args = parse_args() + root = args.root.resolve() + notice, profile_path = load_notice(root, args.year) + paths = expand_requested_paths(root, args.paths) + dry_run = args.dry_run or args.check + + changed = [ + path + for path in paths + if update_file(root, path, notice, dry_run=dry_run) + ] + + rel_profile = profile_path.relative_to(root) + action = "Would update" if dry_run else "Updated" + print(f"Notice source: {rel_profile}") + print(f"{action} {len(changed)} file(s).") + for path in changed: + print(path.as_posix()) + + if args.check and changed: + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From e792c208a76e5da5aa88835071b5ef15262cc219 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 21:06:16 +0100 Subject: [PATCH 08/18] Improve the `update-copyright` skill description --- .agents/skills/update-copyright/SKILL.md | 65 +++--------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/.agents/skills/update-copyright/SKILL.md b/.agents/skills/update-copyright/SKILL.md index 8b1984f0..c7f92345 100644 --- a/.agents/skills/update-copyright/SKILL.md +++ b/.agents/skills/update-copyright/SKILL.md @@ -1,66 +1,15 @@ --- name: update-copyright description: > - Update source file copyright headers from the IntelliJ IDEA copyright profile - in `.idea/copyright/profiles_settings.xml`, resolving the default profile XML, - reading its `notice` option, and replacing `today.year` with the current year. - Automatically apply this skill when source files are modified in a change set. + Update source file copyright headers from the IntelliJ IDEA copyright profile, + replacing `today.year` with the current year. + Automatically apply when source files are modified in a change set. --- # Copyright Update -Use this skill when asked to add, refresh, or normalize source file copyright -statements according to the repository's IntelliJ IDEA copyright profile. +**Command:** `python3 .agents/skills/update-copyright/scripts/update_copyright.py` -## Workflow - -1. Preflight. - - Run `git status --short`; treat existing changes as user-owned. - - Identify the requested scope: explicit files, directories, or all tracked - source files when no scope is given. - -2. Resolve the notice text. - - Read `.idea/copyright/profiles_settings.xml`. - - In the `settings` tag, read the `default` attribute. - - Convert that profile name to the XML filename by replacing each run of - non-alphanumeric characters with `_`, trimming leading/trailing `_`, and - appending `.xml`. - Example: `TeamDev Open-Source` becomes `TeamDev_Open_Source.xml`. - - Read `.idea/copyright/`. - - Use the `value` attribute of the `option` tag whose `name` is `notice`. - - Decode XML/HTML character references, then replace `${today.year}`, - `$today.year`, or bare `today.year` with the current year. - -3. Update files. - - Prefer running `scripts/update_copyright.py`; it resolves the notice, - detects common source comment styles, replaces an existing top copyright - block when present, and inserts one when missing. - - Pass explicit paths when the user gave a scope: - - ```bash - python3 .agents/skills/update-copyright/scripts/update_copyright.py path/to/File.kt scripts/ - ``` - - - With no paths, the script updates tracked source-like files, excluding - generated/build/tooling directories and Gradle wrapper files: - - ```bash - python3 .agents/skills/update-copyright/scripts/update_copyright.py - ``` - - - Use `--dry-run` first for broad changes: - - ```bash - python3 .agents/skills/update-copyright/scripts/update_copyright.py --dry-run - ``` - -4. Verify. - - Re-run `git status --short` and inspect the changed files. - - For broad updates, spot-check at least one block-comment file, one - hash-comment file, and one XML file if present. - - Run a targeted search such as `rg -n "Copyright [0-9]{4}, TeamDev"` when - the profile follows the TeamDev notice format. - -## Report - -Return: `NoticeSource[]`, `UpdatedFiles[]`, `Verification[]`, `Risks[]`. +1. Scope: explicit files/dirs from the user, or all tracked source files if none given. +2. No explicit paths → run with `--dry-run` first, then without. +3. Relay stdout (notice source, file count, changed paths) to the user. From 6097aa121508b49b4e16c470c7045e73b7bad10c Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 21:08:46 +0100 Subject: [PATCH 09/18] Bump Validation -> `2.0.0-SNAPSHOT.413` --- .../src/main/kotlin/io/spine/dependency/local/Validation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt index 549ed2c1..c2a91be6 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt @@ -36,7 +36,7 @@ object Validation { /** * The version of the Validation library artifacts. */ - const val version = "2.0.0-SNAPSHOT.411" + const val version = "2.0.0-SNAPSHOT.413" /** * The last version of Validation compatible with ProtoData. From 46b4c6ca6af5995488aea3499f41b92132d510aa Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 21:10:06 +0100 Subject: [PATCH 10/18] Udate (c) year --- .../src/main/kotlin/io/spine/dependency/local/Validation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt index c2a91be6..a5ef4031 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 921c1ab235e8d376cae3413feb6206f35c35393b Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 21:11:36 +0100 Subject: [PATCH 11/18] Clarify non-adding (c) headers --- .agents/skills/update-copyright/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.agents/skills/update-copyright/SKILL.md b/.agents/skills/update-copyright/SKILL.md index c7f92345..6afc4c7c 100644 --- a/.agents/skills/update-copyright/SKILL.md +++ b/.agents/skills/update-copyright/SKILL.md @@ -13,3 +13,4 @@ description: > 1. Scope: explicit files/dirs from the user, or all tracked source files if none given. 2. No explicit paths → run with `--dry-run` first, then without. 3. Relay stdout (notice source, file count, changed paths) to the user. +4. Never add a copyright header to a file that does not already have one. From 41d52f148b073a5a03ee66e37a8440bdc509c03d Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 21:19:55 +0100 Subject: [PATCH 12/18] Trim skill description --- .agents/skills/move-files/SKILL.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md index 1b908689..43928848 100644 --- a/.agents/skills/move-files/SKILL.md +++ b/.agents/skills/move-files/SKILL.md @@ -1,9 +1,8 @@ --- name: move-files description: > - Move or rename files/directories safely in a repo. Use for relocating source, - test, doc, asset, module, package, or folder paths; preserve history, update - references/build metadata, and verify stale paths are gone. + Move or rename any files/directories in a repo: preserve history, update all + references and build metadata, verify no stale paths remain. --- # Move Files From 53fec20ef534d93fcde59d6397c37a17b60a60fa Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 21:22:39 +0100 Subject: [PATCH 13/18] Compact the `move-files` skill details --- .agents/skills/move-files/SKILL.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md index 43928848..2885f482 100644 --- a/.agents/skills/move-files/SKILL.md +++ b/.agents/skills/move-files/SKILL.md @@ -10,16 +10,15 @@ description: > ## Workflow 1. Preflight. - - Run `git status --short`; treat existing changes as user-owned. + - Run `git status --short`. - Map each `source -> destination`. - - Confirm sources exist and destinations do not, unless merge/overwrite was - explicit. - Classify scope: simple same-module moves stay targeted; package, module, or cross-module moves need broader inspection. + - Ask before ambiguous mappings, destination conflicts, or unclear semantic + package/module changes. 2. Search before moving. - - Search exact old paths, filenames, package/module names, resource paths, and - docs links. + - Search all old identifiers: paths, names, resource refs, doc links. - For Gradle/module/source-set moves, check `settings.gradle.kts`, `build.gradle.kts`, and `buildSrc`. - For Kotlin/Java, update package declarations only when package intent @@ -30,12 +29,9 @@ description: > - Use filesystem moves only for untracked/generated/out-of-git files. - Create parent directories first. - For case-only renames, move through a temporary name. - - Ask before ambiguous mappings, destination conflicts, or unclear semantic - package/module changes. 4. Repair references. - - Update imports, package declarations, build metadata, docs links, resource - paths, fixtures, samples, and scripts. + - Update all references: imports, build metadata, docs, resources, and scripts. - Start search scope narrow: affected directory, then module, then repo-wide. - Prefer precise edits; avoid broad replacements on generic names. @@ -46,8 +42,7 @@ description: > ## Repo Notes -Follow `.agents/project-structure-expectations.md` for module/source-set/test -moves. Prefer `src/main/kotlin` and `src/test/kotlin` unless the module differs. +Follow `.agents/project-structure-expectations.md` for module/source-set/test moves. ## Report From 104cd9724ea26a50e251aebb2ae4d565227a9b8a Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 21:31:29 +0100 Subject: [PATCH 14/18] Improve the reliability of listing files --- .../scripts/update_copyright.py | 16 +++-- .../tests/test_update_copyright.py | 65 +++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 .agents/skills/update-copyright/tests/test_update_copyright.py diff --git a/.agents/skills/update-copyright/scripts/update_copyright.py b/.agents/skills/update-copyright/scripts/update_copyright.py index 35496b72..61757744 100755 --- a/.agents/skills/update-copyright/scripts/update_copyright.py +++ b/.agents/skills/update-copyright/scripts/update_copyright.py @@ -199,11 +199,14 @@ def tracked_files(root: Path) -> list[Path]: if path.is_file() and not is_excluded(path.relative_to(root)) ] - return [ - Path(item) - for item in result.stdout.decode("utf-8").split("\0") - if item - ] + paths = [] + for item in result.stdout.decode("utf-8").split("\0"): + if not item: + continue + path = Path(item) + if (root / path).is_file(): + paths.append(path) + return paths def expand_requested_paths(root: Path, requested: list[str]) -> list[Path]: @@ -328,6 +331,9 @@ def update_file(root: Path, path: Path, notice: str, dry_run: bool) -> bool: try: text = absolute.read_text(encoding="utf-8") + except FileNotFoundError: + print(f"Skipping missing file: {path}", file=sys.stderr) + return False except UnicodeDecodeError: print(f"Skipping non-UTF-8 file: {path}", file=sys.stderr) return False diff --git a/.agents/skills/update-copyright/tests/test_update_copyright.py b/.agents/skills/update-copyright/tests/test_update_copyright.py new file mode 100644 index 00000000..33396a74 --- /dev/null +++ b/.agents/skills/update-copyright/tests/test_update_copyright.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "update_copyright.py" + + +class UpdateCopyrightTest(unittest.TestCase): + def test_default_run_skips_tracked_files_deleted_from_working_tree(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + self.write_profile(root) + source = root / "Foo.java" + source.write_text("class Foo {}\n", encoding="utf-8") + + subprocess.run(["git", "init", "-q"], cwd=root, check=True) + subprocess.run(["git", "add", "Foo.java"], cwd=root, check=True) + source.unlink() + + result = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--root", + str(root), + "--dry-run", + ], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Would update 0 file(s).", result.stdout) + self.assertEqual(result.stderr, "") + + @staticmethod + def write_profile(root: Path) -> None: + copyright_dir = root / ".idea" / "copyright" + copyright_dir.mkdir(parents=True) + (copyright_dir / "profiles_settings.xml").write_text( + '' + '' + "\n", + encoding="utf-8", + ) + (copyright_dir / "Default.xml").write_text( + '' + "" + '" + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + unittest.main() From 9f13635052d008803aa90976c8e0fda0b1b93b57 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Fri, 24 Apr 2026 21:35:34 +0100 Subject: [PATCH 15/18] Make sure (c) header is not added forcibly --- .../scripts/update_copyright.py | 15 +++-- .../tests/test_update_copyright.py | 65 +++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/.agents/skills/update-copyright/scripts/update_copyright.py b/.agents/skills/update-copyright/scripts/update_copyright.py index 61757744..37cb1ac0 100755 --- a/.agents/skills/update-copyright/scripts/update_copyright.py +++ b/.agents/skills/update-copyright/scripts/update_copyright.py @@ -275,20 +275,20 @@ def strip_leading_blank_lines(text: str) -> str: return re.sub(r"^(?:[ \t]*\r?\n)+", "", text) -def strip_existing_header(text: str, style: str) -> str: +def strip_existing_header(text: str, style: str) -> tuple[str, bool]: if style == "block" and text.startswith("/*"): close = text.find("*/") if close != -1: candidate = text[: close + 2] if is_copyright_header(candidate): - return strip_leading_blank_lines(text[close + 2 :]) + return strip_leading_blank_lines(text[close + 2 :]), True if style == "xml" and text.startswith("") if close != -1: candidate = text[: close + 3] if is_copyright_header(candidate): - return strip_leading_blank_lines(text[close + 3 :]) + return strip_leading_blank_lines(text[close + 3 :]), True if style == "hash": lines = text.splitlines(keepends=True) @@ -301,9 +301,9 @@ def strip_existing_header(text: str, style: str) -> str: break candidate = text[:end] if candidate and is_copyright_header(candidate): - return strip_leading_blank_lines(text[end:]) + return strip_leading_blank_lines(text[end:]), True - return text + return text, False def is_copyright_header(text: str) -> bool: @@ -314,12 +314,15 @@ def is_copyright_header(text: str) -> bool: def updated_text(text: str, notice: str, style: str) -> str: + original = text bom = "\ufeff" if text.startswith("\ufeff") else "" if bom: text = text[1:] newline = newline_for(text) prefix, body = split_leading_directive(text, style, newline) - body = strip_existing_header(body, style) + body, had_header = strip_existing_header(body, style) + if not had_header: + return original return bom + prefix + build_header(notice, style, newline) + body diff --git a/.agents/skills/update-copyright/tests/test_update_copyright.py b/.agents/skills/update-copyright/tests/test_update_copyright.py index 33396a74..8770b327 100644 --- a/.agents/skills/update-copyright/tests/test_update_copyright.py +++ b/.agents/skills/update-copyright/tests/test_update_copyright.py @@ -11,6 +11,55 @@ class UpdateCopyrightTest(unittest.TestCase): + def test_default_run_leaves_plain_source_without_header_unchanged(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + self.write_profile(root) + source = root / "Foo.java" + original = "class Foo {}\n" + source.write_text(original, encoding="utf-8") + + subprocess.run(["git", "init", "-q"], cwd=root, check=True) + subprocess.run(["git", "add", "Foo.java"], cwd=root, check=True) + + result = self.run_script(root) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Updated 0 file(s).", result.stdout) + self.assertEqual(result.stderr, "") + self.assertEqual(source.read_text(encoding="utf-8"), original) + + def test_existing_header_is_updated(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + self.write_profile(root) + source = root / "Foo.java" + source.write_text( + "/*\n" + " * Copyright 2024 ACME\n" + " * All rights reserved\n" + " */\n" + "\n" + "class Foo {}\n", + encoding="utf-8", + ) + + result = self.run_script(root, "--year", "2026", "Foo.java") + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Updated 1 file(s).", result.stdout) + self.assertIn("Foo.java", result.stdout) + self.assertEqual(result.stderr, "") + self.assertEqual( + source.read_text(encoding="utf-8"), + "/*\n" + " * Copyright 2026 ACME\n" + " * All rights reserved\n" + " */\n" + "\n" + "class Foo {}\n", + ) + def test_default_run_skips_tracked_files_deleted_from_working_tree(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) @@ -40,6 +89,22 @@ def test_default_run_skips_tracked_files_deleted_from_working_tree(self) -> None self.assertIn("Would update 0 file(s).", result.stdout) self.assertEqual(result.stderr, "") + @staticmethod + def run_script(root: Path, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--root", + str(root), + *args, + ], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + @staticmethod def write_profile(root: Path) -> None: copyright_dir = root / ".idea" / "copyright" From 4ca7f51c17d53f687cc03e623ea8f974c8fad994 Mon Sep 17 00:00:00 2001 From: Alexander Yevsyukov Date: Fri, 24 Apr 2026 23:36:46 +0300 Subject: [PATCH 16/18] Potential fix for pull request finding Make copying `.claude` directory retaining symlinks Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- migrate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrate b/migrate index bc7b908b..ebd63216 100755 --- a/migrate +++ b/migrate @@ -33,7 +33,7 @@ fi echo "Updating \\\`.claude\\\` directory" rm -rf ../.claude if [ -d ".claude" ]; then - cp -R .claude .. + cp -a .claude .. fi # Copies the file or directory passed as the first parameter to the upper directory, From a81f1c26f45dffcb7a609afea4fca2076ee7a29e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:39:37 +0000 Subject: [PATCH 17/18] Guard against out-of-root paths in `expand_requested_paths()` Agent-Logs-Url: https://github.com/SpineEventEngine/config/sessions/0fed50d9-8116-4a24-adc8-0bfe90e122ee Co-authored-by: alexander-yevsyukov <3116444+alexander-yevsyukov@users.noreply.github.com> --- .../update_copyright.cpython-312.pyc | Bin 0 -> 15781 bytes .../scripts/update_copyright.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/update-copyright/scripts/__pycache__/update_copyright.cpython-312.pyc diff --git a/.agents/skills/update-copyright/scripts/__pycache__/update_copyright.cpython-312.pyc b/.agents/skills/update-copyright/scripts/__pycache__/update_copyright.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a199334b476a855ca2bd53c448fcaab17887c137 GIT binary patch literal 15781 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 Avj6}9 literal 0 HcmV?d00001 diff --git a/.agents/skills/update-copyright/scripts/update_copyright.py b/.agents/skills/update-copyright/scripts/update_copyright.py index 37cb1ac0..2dbf8bbc 100755 --- a/.agents/skills/update-copyright/scripts/update_copyright.py +++ b/.agents/skills/update-copyright/scripts/update_copyright.py @@ -218,6 +218,11 @@ def expand_requested_paths(root: Path, requested: list[str]) -> list[Path]: path = (root / item).resolve() if not path.exists(): raise FileNotFoundError(f"Path does not exist: {item}") + if not path.is_relative_to(root): + raise ValueError( + f"Path is outside the repository root: {item!r} " + f"(resolved to {path}, root is {root})" + ) if path.is_dir(): for child in path.rglob("*"): if child.is_file(): @@ -355,7 +360,11 @@ def main() -> int: args = parse_args() root = args.root.resolve() notice, profile_path = load_notice(root, args.year) - paths = expand_requested_paths(root, args.paths) + try: + paths = expand_requested_paths(root, args.paths) + except (FileNotFoundError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 dry_run = args.dry_run or args.check changed = [ From d03516b2d7904d171c1d37f54e6e0463f58f76b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:40:06 +0000 Subject: [PATCH 18/18] Add Python cache entries to .gitignore Agent-Logs-Url: https://github.com/SpineEventEngine/config/sessions/0fed50d9-8116-4a24-adc8-0bfe90e122ee Co-authored-by: alexander-yevsyukov <3116444+alexander-yevsyukov@users.noreply.github.com> --- .../update_copyright.cpython-312.pyc | Bin 15781 -> 0 bytes .gitignore | 4 ++++ 2 files changed, 4 insertions(+) delete mode 100644 .agents/skills/update-copyright/scripts/__pycache__/update_copyright.cpython-312.pyc diff --git a/.agents/skills/update-copyright/scripts/__pycache__/update_copyright.cpython-312.pyc b/.agents/skills/update-copyright/scripts/__pycache__/update_copyright.cpython-312.pyc deleted file mode 100644 index a199334b476a855ca2bd53c448fcaab17887c137..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15781 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 Avj6}9 diff --git a/.gitignore b/.gitignore index 48de9f27..a3e0ec13 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,7 @@ pubspec.lock /tmp .gradle-test-kit/ + +# Python cache +__pycache__/ +*.pyc