chore: optimize AI agent context and implement CI cost controls (#5335)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-02 13:19:11 -05:00
committed by GitHub
parent 92b8f0ddf5
commit 9e396cdecc
11 changed files with 2896 additions and 1368 deletions

View File

@@ -0,0 +1,21 @@
# Agent Session Context - Meshtastic Android
# This is a dated, append-only handover log. Add new entries at the TOP.
# Do NOT edit or remove previous entries — stale state claims cause agent confusion.
# Format: ## YYYY-MM-DD — <summary>
## 2026-05-02 — CI cost-control PR review fixes
- Applied PR review feedback: encoding fixes in sort-strings.py, NUL-delimited staged-files loop
in ai-guardrail.sh, installation instructions added, typo fix in strings.xml, command order
fixed in AGENTS.md, narrowed .aiexclude/.gitattributes patterns, allTests added to SKILL.md.
## 2026-04-XX — Token Mitigation (Phase 1-3)
- `.copilotignore` and `.aiexclude` updated with stricter ignore rules.
- `AGENTS.md` modularized to ~3KB base; detailed rules moved to `.skills/`.
- `scripts/ai-guardrail.sh` added to prevent binary/log leaks (installation: see script header).
- CI Cost Control skill added at `.skills/ci-cost-control/SKILL.md`.
## Golden Context (stable across sessions)
- Always check `.skills/compose-ui/strings-index.txt` before reading `strings.xml`.
- Run `python3 scripts/sort-strings.py` after adding strings to keep the index organized.
- Always check `gh run list` before pushing.
- Pre-commit hook `scripts/ai-guardrail.sh` protects against binary leaks (see script for install).

36
.aiexclude Normal file
View File

@@ -0,0 +1,36 @@
# Standard AI exclusion list for Cursor, Windsurf, etc.
# Mirroring .copilotignore for project-wide token discipline
# Build & Generated
**/build/**
.gradle/
.kotlin/
**/generated/**
# Agent Artifacts
.agent_artifacts/
.agent_refs/
tmp/
*.log
# Media & Binaries
**/*.png
**/*.jpg
**/*.jpeg
**/*.webp
**/*.svg
**/*.ico
**/*.gif
**/*.mp3
**/*.wav
**/*.ogg
**/*.pdf
**/*.ttf
**/*.otf
**/*.jar
**/*.aar
**/*.apk
# Resources (Indexing non-English strings is a token sink)
**/values-*/strings.xml
**/composeResources/**/values-*/*.xml

View File

@@ -1,27 +1,54 @@
# Ignore build artifacts and generated files from Copilot indexing
# Meshtastic Android - GitHub Copilot Ignore List
# This saves context window tokens and prevents Copilot from hallucinating off of minified code.
# Build directories
# ── Build & Generated ─────────────────────────────────────────────────────────
**/build/**
.gradle/
.idea/
# Android generated files
.kotlin/
**/generated/**
.cxx/
.externalNativeBuild/
# Git history & worktrees
.git/
.worktrees/
# Protobuf (Prevents Copilot from suggesting raw protobuf byte buffers)
core/proto/
# Environment and secrets
# ── IDE & Environment ─────────────────────────────────────────────────────────
.idea/
.run/
.claude/
.gemini/
.jdk
local.properties
secrets.properties
*.jks
.DS_Store
# Agent References (Prevents pollution of project space with external code)
# ── Agent Artifacts (Large volumes of logs/images) ───────────────────────────
.agent_artifacts/
# Note: .agent_plans/ is NOT ignored to maintain implementation context.
.agent_refs/
tmp/
*.log
# ── Binary Assets & Media ─────────────────────────────────────────────────────
**/*.png
**/*.jpg
**/*.jpeg
**/*.webp
**/*.svg
**/*.ico
**/*.gif
**/*.mp3
**/*.wav
**/*.ogg
**/*.pdf
**/*.ttf
**/*.otf
**/*.jar
**/*.aar
**/*.apk
# ── External & Submodules ─────────────────────────────────────────────────────
core/proto/
# ── Resources ────────────────────────────────────────────────────────────────
# Ignore translations (reduces churn and indexing tokens)
**/values-*/strings.xml
**/composeResources/**/values*/*.xml

13
.gitattributes vendored Normal file
View File

@@ -0,0 +1,13 @@
# Mark only generated/derived XML as linguist-generated to reduce Copilot PR summary costs.
# Hand-edited resources (layouts, string values) are intentionally excluded so they remain
# visible in diffs and code review.
**/composeResources/**/values-*/*.xml linguist-generated=true
.skills/compose-ui/strings-index.txt linguist-generated=true
# Ensure assets are treated as binary
*.png binary
*.jpg binary
*.webp binary
*.mp3 binary
*.wav binary
*.ogg binary

View File

@@ -0,0 +1,25 @@
# Skill: CI Cost Control & Monitoring
## Description
Guidelines for agents to minimize GitHub Actions compute waste and prevent redundant or failing CI runs.
## Rules
### 1. Check Before You Kick
Before pushing code that triggers a CI workflow, you **MUST** check if a relevant run is already in progress:
```bash
gh run list --branch $(git branch --show-current) --limit 5
```
- If a run is pending/running for your current state, **DO NOT** push again unless you are fixing a specific CI failure.
- Cancel redundant runs if your new push supersedes them: `gh run cancel <run_id>`.
### 2. Local First
NEVER use CI as a "remote compiler."
- You must run `./gradlew spotlessApply spotlessCheck detekt test allTests` locally before pushing.
- If local tests fail, CI **will** fail. Do not waste the tokens or the compute.
### 3. Modular CI Invocations
When using the `/delegate` or autonomous PR tools, explicitly limit the scope of the CI check if the tool supports it. Avoid running the full multi-OS desktop matrix for a simple documentation fix.
## Monitoring
Use `gh run view <run_id>` to inspect failures. Do not re-run a whole suite if only one shard failed due to a known flake; use `gh run rerun --failed`.

View File

@@ -64,3 +64,8 @@ When reviewing code, meticulously verify the following categories. Flag any devi
2. **Reference the Docs:** Cite `AGENTS.md` and project architecture playbooks to justify change requests (e.g., "Per AGENTS.md, `java.io.*` cannot be used in `commonMain`; please migrate to Okio").
3. **Enforce Build Health:** Remind authors to run `./gradlew test allTests` locally to verify changes, especially since KMP `test` tasks are ambiguous.
4. **Praise Good Patterns:** Acknowledge correct usage of complex architecture requirements, like proper Navigation 3 scene transitions or elegant `commonMain` helper extractions.
## Git & PR Hygiene Rules
- **Commit Hygiene:** Squash fixup/polish/review-feedback commits before opening a PR. Each commit should represent a logical, self-contained unit of work — not a back-and-forth conversation.
- **PR Descriptions:** Keep PR descriptions concise and scannable. State *what changed* and *why*, not a per-commit play-by-play. Use a short summary paragraph followed by a bullet list of changes. Avoid tables, headers-per-commit, or verbose breakdowns. Reference the `meshtastic/firmware` repo PRs for tone and style.
- **PR Titles:** Use conventional commit format: `feat(scope):`, `fix(scope):`, `refactor(scope):`, `chore(scope):`. Keep titles under ~72 characters.

1306
.skills/compose-ui/strings-index.txt generated Normal file
View File

File diff suppressed because it is too large Load Diff

135
AGENTS.md
View File

@@ -1,113 +1,46 @@
# Meshtastic Android - Unified Agent & Developer Guide
<role>
You are an expert Android and Kotlin Multiplatform (KMP) engineer working on Meshtastic-Android, a decentralized mesh networking application. You must maintain strict architectural boundaries, use Modern Android Development (MAD) standards, and adhere to Compose Multiplatform and JetBrains Navigation 3 patterns.
You are an expert Android/KMP engineer. Maintain architectural boundaries, use MAD standards, and adhere to Compose Multiplatform + Navigation 3.
</role>
<context_and_memory>
- **Project Goal:** Decouple business logic from the Android framework for seamless multi-platform execution (Android, Desktop, iOS) while maintaining a high-performance native Android experience.
- **Language & Tech:** Kotlin 2.3+ (JDK 21 REQUIRED), Gradle Kotlin DSL, Ktor, Okio, Room KMP.
- **Core Architecture:**
- `commonMain` is pure KMP. `androidMain` is strictly for Android framework bindings.
- App root DI and graph assembly live in the `app` and `desktop` host shells.
- **Skills Directory:** You **MUST** consult the relevant `.skills/` module before executing work:
- `.skills/project-overview/` - Codebase map, module directory, namespacing, environment setup, troubleshooting.
- `.skills/kmp-architecture/` - Bridging, expect/actual, source-sets, catalog aliases, build-logic conventions.
- `.skills/compose-ui/` - Adaptive UI, placeholders, string resources.
- `.skills/navigation-and-di/` - JetBrains Navigation 3 & Koin 4.2+ annotations.
- `.skills/testing-ci/` - Validation commands, CI pipeline architecture, CI Gradle properties.
- `.skills/implement-feature/` - Step-by-step feature workflow.
- `.skills/code-review/` - PR validation checklist.
- `.skills/new-branch/` - Canonical recipe for branching off upstream/main and rebasing stale PRs.
- **Active Status:** Read `docs/kmp-status.md` and `docs/roadmap.md` to understand the current KMP migration epoch.
- **Project Goal:** Decouple business logic from Android for multi-platform (Android, Desktop, iOS).
- **Tech:** Kotlin 2.3+ (JDK 21), Ktor, Okio, Room KMP, Koin 4.2+.
- **Agent Memory:** Consult `.agent_memory/session_context.md` for the latest task-specific handovers and project state.
- **Skills Directory (CONSULT THESE FIRST):**
- `.skills/project-overview/` - Codebase map, namespacing, **Bootstrap Steps**.
- `.skills/kmp-architecture/` - Expect/actual, source-sets, conventions.
- `.skills/compose-ui/` - Adaptive UI, **String Resources (consult strings-index.txt first)**.
- `.skills/navigation-and-di/` - Navigation 3 & Koin annotations.
- `.skills/testing-ci/` - Validation commands, **CI Architecture**.
- `.skills/ci-cost-control/` - **CI Budgeting & Monitoring**.
- `.skills/implement-feature/` - Feature workflow.
- `.skills/code-review/` - **PR & Commit Hygiene**, validation checklist.
- `.skills/new-branch/` - Branching and rebasing recipes.
</context_and_memory>
<process>
- **Workspace Bootstrap (MUST run first):** Before executing any Gradle task in a new workspace, agents MUST automatically:
1. **Find the Android SDK**`ANDROID_HOME` is often unset in agent worktrees. Probe `~/Library/Android/sdk`, `~/Android/Sdk`, and `/opt/android-sdk`. Export the first one found. If none exist, ask the user.
2. **Init the proto submodule** — Run `git submodule update --init`. The `core/proto/src/main/proto` submodule contains Protobuf definitions required for builds.
3. **Init secrets** — If `local.properties` does not exist, copy `secrets.defaults.properties` to `local.properties`. Without this the `google` flavor build fails.
- **Think First:** Reason through the problem before writing code. For complex KMP tasks involving multiple modules or source sets, outline your approach step-by-step before executing.
- **Plan Before Execution:** Use the git-ignored `.agent_plans/` directory to write markdown implementation plans (`plan.md`) and Mermaid diagrams (`.mmd`) for complex refactors before modifying code.
- **Atomic Execution:** Follow your plan step-by-step. Do not jump ahead. Use TDD where feasible (write `commonTest` fakes first).
- **Baseline Verification:** Always instruct the user (or use your CLI tools) to run the baseline check before finishing:
```
./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests
```
> **Why both `test` and `allTests`?** In KMP modules, `test` is ambiguous and Gradle silently skips them. `allTests` is the KMP lifecycle task that covers KMP modules. Conversely, `allTests` does NOT cover pure-Android modules (`:app`, `:core:api`), so both tasks are required.
> For KMP cross-platform compilation, also run `./gradlew kmpSmokeCompile` (compiles all KMP modules for JVM + iOS Simulator — used by CI's `lint-check` job).
</process>
<agent_tools>
- **Codebase Search:** Use whatever search and navigation tools your environment provides (file search, grep/ripgrep, symbol lookup, semantic search, etc.) to map out project boundaries before coding. Prefer `rg` (ripgrep) over `grep` or `find` for raw text search.
- **Terminal Pagers:** When running shell commands like `git diff` or `git log`, ALWAYS use `--no-pager` (e.g., `git --no-pager diff`) to prevent getting stuck in an interactive prompt.
- **Fetch Up-to-Date Docs:** If your environment supports web search, MCP servers, or documentation lookup tools, actively query them for the latest documentation on Koin 4.x, JetBrains Navigation 3, and Compose Multiplatform 1.11.
- **Clone Reference Repos:** If documentation is insufficient, use shell commands to clone bleeding-edge KMP dependency repositories into the local `.agent_refs/` directory (git-ignored) to inspect their source and test suites. Recommended:
- `https://github.com/JetBrains/kotlin-multiplatform-dev-docs` (Official Docs)
- `https://github.com/InsertKoinIO/koin` (Koin Annotations 4.x)
- `https://github.com/JetBrains/compose-multiplatform` (Navigation 3, Adaptive UI)
- `https://github.com/JuulLabs/kable` (BLE)
- `https://github.com/coil-kt/coil` (Coil 3 KMP)
- `https://github.com/ktorio/ktor` (Ktor Networking)
- **Formatting Hooks:** Always run `./gradlew spotlessApply` as an automatic formatting hook to fix style violations after editing.
</agent_tools>
<documentation_sync>
`AGENTS.md` is the single source of truth for agent instructions. Agent-specific files redirect here:
- `.github/copilot-instructions.md` — Copilot redirect to `AGENTS.md`.
- `CLAUDE.md` — Claude Code entry point; imports `AGENTS.md` via `@AGENTS.md` and adds Claude-specific instructions.
- `GEMINI.md` — Gemini redirect to `AGENTS.md`. Gemini CLI also configured via `.gemini/settings.json` to read `AGENTS.md` directly.
Do NOT duplicate content into agent-specific files. When you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update `AGENTS.md`, `.skills/`, and `docs/kmp-status.md` as needed.
</documentation_sync>
<process_essentials>
- **Think First:** Read only what you need. Consult indices (like `strings-index.txt`) before reading large files.
- **Hygiene:** Run `python3 scripts/sort-strings.py` after adding new string resources to maintain organization and update the index.
- **Memory Persistence:** Update `.agent_memory/session_context.md` at the end of every session or major task.
- **Bootstrap First:** Run the mandatory bootstrap steps in `.skills/project-overview/SKILL.md` before any build.
- **Plan Before Execution:** Use `.agent_plans/` (git-ignored) for complex refactors.
- **Baseline Verification:** Always run: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`
</process_essentials>
<rules>
- **No Lazy Coding:** DO NOT use placeholders like `// ... existing code ...`. Always provide complete, valid code blocks for the sections you modify to ensure correct diff application.
- **No Framework Bleed:** NEVER import `java.*` or `android.*` in `commonMain`. Use KMP equivalents: `Okio` for `java.io.*`, `kotlinx.coroutines.sync.Mutex` for `java.util.concurrent.locks.*`, `atomicfu` or Mutex-guarded `mutableMapOf()` for `ConcurrentHashMap`. Use `org.meshtastic.core.common.util.ioDispatcher` instead of `Dispatchers.IO` directly.
- **Koin Annotations:** Use `@Single`, `@Factory`, and `@KoinViewModel` inside `commonMain` instead of manual constructor trees. Do not enable A1 module compile safety — A3 full-graph validation (`VerifyModule`) is the correct approach because interfaces and implementations live in separate modules. Always register new feature modules in **both** `AppKoinModule.kt` and `DesktopKoinModule.kt`; they are not auto-activated.
- **CMP Over Android:** Use `compose-multiplatform` constraints. `stringResource` only supports `%N$s` and `%N$d` — pre-format floats with `NumberFormatter.format()` from `core:common` and pass as `%N$s`. In ViewModels/coroutines use `getStringSuspend(Res.string.key)`; never blocking `getString()`. Always use `MeshtasticNavDisplay` (not raw `NavDisplay`) as the navigation host, and `NavigationBackHandler` (not Android's `BackHandler`) for back gestures in shared code.
- **ProGuard:** When adding a reflection-heavy dependency, add keep rules to **both** `app/proguard-rules.pro` and `desktop/proguard-rules.pro` and verify release builds.
- **Always Check Docs:** If unsure about an abstraction, search `core:ui/commonMain` or `core:navigation/commonMain` before assuming it doesn't exist.
- **Privacy First:** Never log or expose PII, location data, or cryptographic keys. Meshtastic is used for sensitive off-grid communication — treat all user data with extreme caution.
- **Dependency Discipline:** Never add a library without first checking `libs.versions.toml` and justifying its inclusion against the project's size and complexity goals. Prefer removing dependencies over adding them.
- **Zero Lint Tolerance:** A task is incomplete if `detekt` fails or `spotlessCheck` does not pass for touched modules.
- **Read Before Refactoring:** When a pattern contradicts best practices, analyze whether it is legacy debt or a deliberate architectural choice before proposing a change.
- **Verify Before Push:** Treat any "push", "commit and push", or "push and pr" request as **verify-then-push**. Before `git push`, run `./gradlew spotlessApply detekt` (and the relevant `:module:test` / `:module:lint<Flavor>Debug` for touched modules). CI has repeatedly failed on `UnusedParameter`, `CyclomaticComplexMethod`, and `MagicNumber` from skipping this step. Only push on green; if a check fails, fix it before pushing.
- **Never Touch Protos or Secrets:** `core/proto/src/main/proto` is an upstream submodule — **do not modify** any `.proto` file. If a feature request requires a proto change, stop and report it as upstream (label issue `upstream`, point at `meshtastic/protobufs`). Likewise, never `git add` `app/google-services.json`, `local.properties`, `secrets.properties`, or any `*.keystore` / `*.jks` file — these are gitignored and contain secrets.
- **Multi-Flavor Install Hygiene:** When using the `android` CLI MCP to install/run on a connected device, the `fdroid` (`com.geeksville.mesh`) and `google` (`com.geeksville.mesh.google`) flavors have different signatures and **cannot coexist**. Before any install: pick a flavor explicitly, force-stop and uninstall the other flavor on every connected device, then install. Stale installs of the other flavor are a recurring source of "the fix didn't work" red herrings.
- **Verify UI With Annotated Screenshots:** For any UI/UX task, do **not** claim a fix works based on logs or assumed state. Capture an annotated screenshot via the `android` CLI MCP (or its annotated-screenshot tool) on a real connected device, and inspect the result before reporting back.
- **Branch Scope Discipline:** If a working branch grows beyond ~5 logical commits, crosses unrelated concerns, or accumulates a large blast radius, proactively propose a fresh branch off `upstream/main` and cherry-pick only the high-signal, low-risk changes (see `.skills/new-branch/SKILL.md`). Don't keep piling onto a sprawling branch.
- **Token Hygiene:** NEVER read binary files (PNG, MP3, etc.) or large non-code resources unless essential. Use file paths to reason about assets.
- **Context Discipline:** Limit your context to relevant modules. Do not "vacuum" the entire codebase for localized fixes.
- **No Lazy Coding:** DO NOT use placeholders like `// ... existing code ...`. Provide complete, valid code blocks.
- **No Framework Bleed:** NEVER import `java.*` or `android.*` in `commonMain`. Use KMP equivalents (Okio, Mutex, atomicfu).
- **CMP Over Android:** Use `compose-multiplatform` constraints. Pre-format floats with `NumberFormatter.format()`. Use `MeshtasticNavDisplay` and `NavigationBackHandler`.
- **Zero Lint Tolerance:** Task is incomplete if `detekt` or `spotlessCheck` fails.
- **Verify Before Push:** Treat any "push" as verify-then-push. CI has failed repeatedly due to skipped local checks.
- **Never Touch Protos or Secrets:** `core/proto` is an upstream submodule. Secrets are git-ignored.
- **Privacy First:** Never log or expose PII, location, or cryptographic keys.
</rules>
<copilot_cli_workflow>
These tips apply when the agent is the GitHub Copilot CLI. Other agent runtimes may ignore this
section.
- **Delegate long autonomous work.** For sweeping audits, multi-hour investigations, or "fleet"
prompts (*"investigate why X is broken on release"*, *"audit the diff since tag vX.Y.Z"*,
*"review the codebase for best practices against spec Z"*), prefer `/delegate` so the GitHub
cloud agent opens a PR while the user keeps working locally. Don't tie up an interactive
session on work that can run unattended.
- **Use `/research` for "latest hotness" prompts.** When the user asks for *"the latest scoop"*
on Kotlin / KMP / Compose / Koin trends, the built-in `/research` slash command performs deep
research across GitHub and the web with better source grounding than an ad-hoc prompt.
- **Use `/plan` mode for "noodle it out" prompts.** When the user asks for an implementation
plan, a "walk me through next steps", or explicitly says "don't do anything yet" — switch to
plan mode (Shift+Tab or `/plan`). Plans persist in the session workspace and keep the agent
from prematurely editing files. Continue to write long-form plans and Mermaid diagrams to
`.agent_plans/` (git-ignored) for multi-module refactors.
- **`/share` audit and review outputs.** After large audits, PR safety reviews, or release-cycle
quality passes, offer `/share` to export the findings to a gist or markdown file. These
reports are valuable artifacts — don't let them die in session history.
- **Prefer `/rewind` or `ctrl+s` over retyping.** If a turn went sideways, `/rewind` reverts
file changes and the turn; `ctrl+s` submits while preserving the input for quick iteration.
Avoid re-issuing the same prompt verbatim.
- **New-branch flow lives in a skill.** When the user says "fresh branch off fetched origin/main"
or "rebase PR #NNNN", consult `.skills/new-branch/SKILL.md` rather than re-deriving the recipe.
</copilot_cli_workflow>
<git_and_prs>
- **Commit Hygiene:** Squash fixup/polish/review-feedback commits before opening a PR. Each commit should represent a logical, self-contained unit of work — not a back-and-forth conversation.
- **PR Descriptions:** Keep PR descriptions concise and scannable. State *what changed* and *why*, not a per-commit play-by-play. Use a short summary paragraph followed by a bullet list of changes. Avoid tables, headers-per-commit, or verbose breakdowns. Reference the `meshtastic/firmware` repo PRs for tone and style.
- **PR Titles:** Use conventional commit format: `feat(scope):`, `fix(scope):`, `refactor(scope):`, `chore(scope):`. Keep titles under ~72 characters.
</git_and_prs>
<documentation_sync>
`AGENTS.md` is the source of truth. Redirects: `.github/copilot-instructions.md`, `CLAUDE.md`, `GEMINI.md`.
</documentation_sync>

View File

File diff suppressed because it is too large Load Diff

53
scripts/ai-guardrail.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Meshtastic AI Guardrail - Prevent binary/log leaks in commits
#
# INSTALLATION
# ------------
# Option 1 (recommended): set core.hooksPath so all devs share it automatically:
# git config core.hooksPath scripts/hooks
# mkdir -p scripts/hooks
# ln -sf ../../ai-guardrail.sh scripts/hooks/pre-commit
#
# Option 2: copy/symlink directly into the local .git directory:
# ln -sf ../../scripts/ai-guardrail.sh .git/hooks/pre-commit
# chmod +x .git/hooks/pre-commit
#
# To run manually: bash scripts/ai-guardrail.sh
# List of patterns that should NEVER be committed by an AI Agent
FORBIDDEN_PATTERNS=(
"\.log$"
"\.png$"
"\.jpg$"
"\.jpeg$"
"\.webp$"
"\.mp3$"
"tmp/"
"\.agent_artifacts/"
"build/"
"google-services\.json$"
"local\.properties$"
"secrets\.properties$"
)
VIOLATIONS=()
while IFS= read -r -d '' file; do
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
if [[ $file =~ $pattern ]]; then
VIOLATIONS+=("$file (matched $pattern)")
fi
done
done < <(git diff --cached --name-only -z)
if [ ${#VIOLATIONS[@]} -ne 0 ]; then
echo "❌ AI GUARDRAIL VIOLATION: Staged files contain high-token or sensitive artifacts:"
for violation in "${VIOLATIONS[@]}"; do
echo " - $violation"
done
echo ""
echo "Please unstage these files before committing. Use .copilotignore to prevent this in the future."
exit 1
fi
exit 0

98
scripts/sort-strings.py Normal file
View File

@@ -0,0 +1,98 @@
import xml.etree.ElementTree as ET
import os
import re
# Meshtastic String Sorter & Indexer
# Usage: python3 scripts/sort-strings.py
# This script alphabetizes strings.xml, adds prefix markers, and regenerates strings-index.txt.
def sort_strings(xml_path, index_path):
print(f"Reading {xml_path}...")
with open(xml_path, 'r', encoding='utf-8', newline='\n') as f:
content = f.read()
# Extract license header
header_match = re.search(r'^(.*?)<resources>', content, re.DOTALL)
header = header_match.group(1) if header_match else '<?xml version="1.0" encoding="utf-8"?>\n'
# Parse XML
tree = ET.parse(xml_path)
root = tree.getroot()
# Extract elements and their names
elements = []
for child in root:
name = child.get('name')
if name:
elements.append((name, child))
# Sort elements by name
elements.sort(key=lambda x: x[0])
# Group by prefix
grouped_elements = {}
for name, element in elements:
prefix = name.split('_')[0]
if prefix not in grouped_elements:
grouped_elements[prefix] = []
grouped_elements[prefix].append(element)
# Reconstruct XML and prepare Index
new_root = ET.Element('resources')
index_lines = []
sorted_prefixes = sorted(grouped_elements.keys())
for prefix in sorted_prefixes:
group = grouped_elements[prefix]
if len(group) >= 5:
marker = prefix.upper()
new_root.append(ET.Comment(f' {marker} '))
index_lines.append(f"### {marker} ###")
for element in group:
new_root.append(element)
index_lines.append(element.get('name'))
# Pretty print helper
def prettify(elem, level=0):
indent = " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = "\n" + (level + 1) * indent
if not elem.tail or not elem.tail.strip():
elem.tail = "\n" + level * indent
for i, child in enumerate(elem):
prettify(child, level + 1)
if i < len(elem) - 1:
if not child.tail or not child.tail.strip():
child.tail = "\n" + (level + 1) * indent
else:
if not child.tail or not child.tail.strip():
child.tail = "\n" + level * indent
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = "\n" + level * indent
prettify(new_root)
# Write XML
xml_str = ET.tostring(new_root, encoding='unicode')
final_content = header + xml_str + '\n'
with open(xml_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(final_content)
print(f"Successfully sorted {xml_path}")
# Write Index
with open(index_path, 'w', encoding='utf-8', newline='\n') as f:
f.write('\n'.join(index_lines) + '\n')
print(f"Successfully regenerated {index_path}")
if __name__ == "__main__":
xml_file = 'core/resources/src/commonMain/composeResources/values/strings.xml'
index_file = '.skills/compose-ui/strings-index.txt'
if os.path.exists(xml_file):
sort_strings(xml_file, index_file)
else:
print(f"Error: {xml_file} not found.")