mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-10 13:25:20 -04:00
chore(claude): history-driven agent-tooling hardening (#6195)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,14 @@ cd "$(git rev-parse --show-toplevel)" && pwd && export ANDROID_HOME="${ANDROID_H
|
||||
```
|
||||
If a build complains `local.properties` is missing (Google-flavor tasks), `cp secrets.defaults.properties local.properties` first — it's git-ignored. Do not `cd` elsewhere mid-command.
|
||||
|
||||
## Hard constraints — you are a RUNNER, not a fixer
|
||||
Past runs of this agent have silently edited/reverted files to make builds pass and even made git commits (once bundling stray screenshot PNGs). Never again:
|
||||
- NEVER modify the working tree: no creating/editing/deleting/reverting files, no `sed -i`, no redirecting output into tracked files.
|
||||
- NEVER run git write commands: no `commit`, `add`, `checkout --`, `restore`, `stash`, `clean`, `reset`. Read-only git (`status`, `diff`, `log`) is fine.
|
||||
- The ONLY permitted writes are bootstrap: `export ANDROID_HOME=...` and `cp secrets.defaults.properties local.properties` (git-ignored).
|
||||
- If the build fails, REPORT it — do not attempt any fix, however trivial.
|
||||
- If a Gradle task itself dirties tracked files (e.g. `allTests` regenerates `docs/assets/screenshots/*.png` on this machine), leave them dirty and say so in NOTES — do not revert.
|
||||
|
||||
## How to run
|
||||
- Run exactly the task(s) the caller specified. Do not add `clean` unless asked.
|
||||
- KMP test gotcha: KMP modules use `:module:allTests`; pure-Android/JVM modules (`app`, `core:api`, `core:barcode`) use `:module:testFdroidDebugUnitTest`. If the caller's task name looks wrong for the module type, run what they asked, then note the likely correct name in your report.
|
||||
|
||||
@@ -84,4 +84,43 @@ $out"
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Advisory Compose-pitfall checks for Kotlin edits (warn-only, never block) ---
|
||||
case "$file_path" in
|
||||
*.kt) : ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
case "$file_path" in
|
||||
*Preview*|*commonTest*|*androidUnitTest*|*/test/*|*/androidTest/*) exit 0 ;;
|
||||
esac
|
||||
|
||||
notes=""
|
||||
|
||||
# Lazy-list duplicate-key crash — shipped TWICE (bare telemetry.time; bare
|
||||
# node.num). Flag key lambdas built on those exact fields.
|
||||
risky=$(grep -nE 'key[[:space:]]*=[[:space:]]*\{[[:space:]]*([A-Za-z_][A-Za-z0-9_]*[[:space:]]*->[[:space:]]*)?[A-Za-z_][A-Za-z0-9_.]*\.(num|time)[[:space:]]*\}' "$file_path" 2>/dev/null)
|
||||
if [ -n "$risky" ]; then
|
||||
notes="Lazy-list key built on .num/.time — this exact pattern shipped two production dup-key crashes (bare telemetry.time, fixed with \"\${time}_\$index\"; bare node.num, fixed with distinctBy since _\$index breaks animateItem). Keys must be unique across the submitted list — dedupe the source list or compose the key:
|
||||
$risky"
|
||||
fi
|
||||
|
||||
# Hardcoded user-facing strings — Crowdin never sees literals (caught on PR
|
||||
# #6143). Main source sets only; matching surrounding hardcoded code is not
|
||||
# an excuse (that's a latent bug, not a pattern).
|
||||
case "$file_path" in
|
||||
*/commonMain/*|*/androidMain/*)
|
||||
# Two shapes: inline Text("...") / Text(text = "..."), and the multiline
|
||||
# form where 'text = "..."' sits on its own line inside a formatted call.
|
||||
hardcoded=$(grep -nE '(Text\(|text[[:space:]]*=)[[:space:]]*"[A-Za-z]' "$file_path" 2>/dev/null)
|
||||
if [ -n "$hardcoded" ]; then
|
||||
[ -n "$notes" ] && notes="$notes
|
||||
|
||||
"
|
||||
notes="${notes}Possible hardcoded user-facing string(s) — user-facing text must use stringResource(Res.string.x) or Crowdin never sees it (PR #6143). Check .skills/compose-ui/strings-index.txt for an existing string first:
|
||||
$hardcoded"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -n "$notes" ] && emit_context "$notes"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -36,6 +36,13 @@ ask() { # $1 = reason; prompt the user to confirm
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Gradle gates apply ONLY to this project. Without this, pushes/commits in
|
||||
# OTHER repos got blocked by a failing ./gradlew (bit us: had to evade with
|
||||
# `git -C <path> push`). $1 = repo root; false -> caller should fail open.
|
||||
is_this_repo() {
|
||||
[ -x "$1/gradlew" ] && grep -qi meshtastic "$1/settings.gradle.kts" 2>/dev/null
|
||||
}
|
||||
|
||||
# --- 2. Destructive-op confirmation (cheap checks first) --------------------
|
||||
if printf '%s' "$cmd" | grep -q 'git push' \
|
||||
&& printf '%s' "$cmd" | grep -Eq -- '(--force([^-]|$)|[[:space:]]-f([[:space:]]|$))'; then
|
||||
@@ -51,6 +58,7 @@ if printf '%s' "$cmd" | grep -q 'git push'; then
|
||||
cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
|
||||
[ -z "$cwd" ] && cwd="$PWD"
|
||||
repo_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || exit 0
|
||||
is_this_repo "$repo_root" || exit 0
|
||||
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
|
||||
# ponytail: detekt-only gate — test/allTests deliberately NOT run here (minutes
|
||||
# per push is too costly; baseline stays the developer's job). Ceiling: detekt
|
||||
@@ -76,6 +84,17 @@ cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
|
||||
[ -z "$cwd" ] && cwd="$PWD"
|
||||
repo_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || exit 0
|
||||
[ -n "$repo_root" ] || exit 0
|
||||
is_this_repo "$repo_root" || exit 0
|
||||
|
||||
# Staged screenshot PNGs: allTests regenerates docs/assets/screenshots/*.png on
|
||||
# this machine (host-render diff), and gradle-runner once auto-committed strays.
|
||||
# Confirm they are intentional UI-change screenshots before they ride along.
|
||||
shots=$(git -C "$repo_root" diff --cached --name-only -- 'docs/assets/screenshots/*.png' 2>/dev/null)
|
||||
if [ -n "$shots" ]; then
|
||||
ask "Staged screenshot PNGs detected:
|
||||
$shots
|
||||
allTests regenerates these on this machine — if they are NOT intentional UI-change screenshots, unstage and restore them (git restore --staged --worktree -- docs/assets/screenshots) before committing. Flagged by .claude/hooks/pre-bash-guard.sh"
|
||||
fi
|
||||
|
||||
# Staged Kotlin files (added/copied/modified/renamed). Nothing staged -> no-op.
|
||||
staged=$(git -C "$repo_root" diff --cached --name-only --diff-filter=ACMR -- '*.kt' '*.kts' 2>/dev/null)
|
||||
|
||||
50
.claude/hooks/subagent-audit.sh
Executable file
50
.claude/hooks/subagent-audit.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# PostToolUse hook (Task|Agent) for Meshtastic-Android.
|
||||
#
|
||||
# Tripwire for subagent side effects: gradle-runner has silently edited/reverted
|
||||
# files to make builds pass AND made git commits (once bundling stray screenshot
|
||||
# PNGs). After every subagent returns, surface HEAD + dirty files to the main
|
||||
# loop as additionalContext — but only when there is something to see: a dirty
|
||||
# tree, or a HEAD commit younger than 15 minutes (possibly made by the subagent
|
||||
# that just finished).
|
||||
#
|
||||
# ponytail: stateless — can't diff against pre-subagent state, so your own
|
||||
# in-progress edits show up too; the note says so. Snapshot-before/after if the
|
||||
# noise ever matters.
|
||||
#
|
||||
# FAILS OPEN: any error -> exit 0 with no output.
|
||||
|
||||
input=$(cat)
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
|
||||
[ -z "$cwd" ] && cwd="$PWD"
|
||||
repo_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || exit 0
|
||||
|
||||
dirty=$(git -C "$repo_root" status --porcelain 2>/dev/null | head -20)
|
||||
head_line=$(git -C "$repo_root" log -1 --format='%h %s (%cr)' 2>/dev/null)
|
||||
head_ct=$(git -C "$repo_root" log -1 --format=%ct 2>/dev/null)
|
||||
[ -n "$head_ct" ] || head_ct=0
|
||||
head_age=$(( $(date +%s) - head_ct ))
|
||||
|
||||
fresh_commit=""
|
||||
[ "$head_age" -lt 900 ] && fresh_commit="yes"
|
||||
|
||||
[ -z "$dirty" ] && [ -z "$fresh_commit" ] && exit 0
|
||||
|
||||
note="Subagent-audit (.claude/hooks/subagent-audit.sh) — post-subagent tree check:
|
||||
HEAD: $head_line"
|
||||
if [ -n "$fresh_commit" ]; then
|
||||
note="$note
|
||||
^ HEAD is under 15 min old. If YOU did not make this commit, the subagent did (gradle-runner has done this before) — inspect with 'git show --stat' before building on it."
|
||||
fi
|
||||
if [ -n "$dirty" ]; then
|
||||
note="$note
|
||||
Dirty files (first 20):
|
||||
$dirty
|
||||
Expected if these are your own in-progress edits. If the subagent was only supposed to BUILD/TEST, verify it didn't edit or revert files to force a pass (git diff)."
|
||||
fi
|
||||
|
||||
jq -n --arg c "$note" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$c}}'
|
||||
exit 0
|
||||
@@ -22,7 +22,18 @@
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit.sh\"",
|
||||
"timeout": 60,
|
||||
"statusMessage": "Post-edit checks (strings sort / metadata length / module CI filter)"
|
||||
"statusMessage": "Post-edit checks (strings sort / metadata length / module CI filter / Compose pitfalls)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Task|Agent",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-audit.sh\"",
|
||||
"timeout": 15,
|
||||
"statusMessage": "Subagent-audit (did the subagent touch the tree?)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
21
.claude/skills/crashlytics-triage/SKILL.md
Normal file
21
.claude/skills/crashlytics-triage/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: crashlytics-triage
|
||||
description: Triage current Meshtastic-Android crashes in Firebase Crashlytics — establish the right version filter (topVersions first, then topIssues filtered to "X.Y.Z (versionCode)"), fan out one crash-investigator subagent per top issue in parallel, and return a distilled verdict table. Use for "what's crashing", "triage Crashlytics", or "is build NNNN healthy" sweeps; for a single known issue id, dispatch crash-investigator directly instead.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# crashlytics-triage
|
||||
|
||||
Crashlytics sweep for the **meshutil** Firebase project (`484268767777`), prod app id `1:484268767777:android:70d9bffeca6efe05334160`. Datadog RUM is the *other* backend (high-volume logged errors — use `datadog-rum-investigator` there); Crashlytics is the low-volume real-crash signal. If Firebase MCP auth fails, repair the local Firebase MCP authentication (the configured session account has access).
|
||||
|
||||
## 1. Version context first — never guess the filter string
|
||||
Call `crashlytics_get_report` for **topVersions with NO filter**. This yields the exact display names — the version filter format is `"X.Y.Z (versionCode)"` and hand-built strings silently match nothing. Pick the target version(s): the argument if given, else the newest **production** version with meaningful session volume. topVersions doesn't say which track a versionCode shipped on — map candidate versionCodes to releases via `gh release list` (release names embed the versionCode; never hand-arithmetic) before picking, then use the exact topVersions display name as the filter.
|
||||
|
||||
## 2. Top issues for that version
|
||||
`crashlytics_get_report` topIssues filtered to the exact display name from step 1. Take the top ~5 (or the requested count) by event count. Note event counts and affected-user counts.
|
||||
|
||||
## 3. Fan out — one crash-investigator per issue, in parallel
|
||||
Dispatch the `crash-investigator` agent for each issue **in a single message** so they run concurrently. Give each: the issue id, the version display name, and the ask (root-cause hypothesis + fix area + whether it's already fixed/known). Historical patterns to investigate — hints, not automatic classifications: cluster-renderer lifecycle (fix shipped in 29321034), MQTT/TLS ktor write (fixed, lingering 2.7.14 users), LazyColumn dup-key (two prior instances). Each investigator must verify the crashing versionCode against the fix's release before reporting "known-fixed-residual".
|
||||
|
||||
## 4. Verdict
|
||||
One table: issue → crash count/users → root-cause hypothesis → status (NEW / known-fixed-residual / regression) → fix area. Flag anything that warrants a hotfix vs. next-release. No raw stack traces in the summary.
|
||||
34
.claude/skills/pr/SKILL.md
Normal file
34
.claude/skills/pr/SKILL.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: pr
|
||||
description: Push the current branch and open a draft PR for Meshtastic-Android the repo way — baseline verified first, body drafted per .github/copilot-pull-request-instructions.md (WHY-first, categorized changes), screenshots embedded via commit-pinned raw URLs. Use whenever work is ready to go up as a PR.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# pr
|
||||
|
||||
Opens a PR the way this repo expects. The SOP lives in `.github/copilot-pull-request-instructions.md` — **read it now and follow it**; this skill only adds the steps around it.
|
||||
|
||||
## 1. Pre-flight
|
||||
- Target branch is `main` unless told otherwise.
|
||||
- On the default branch? Create a feature branch first.
|
||||
- Baseline must be green **this session** (`spotlessApply spotlessCheck detekt assembleDebug test allTests`). If it hasn't run since the last code change, run `/baseline` first — CI has failed repeatedly on skipped local checks. (The pre-push hook only gates detekt.)
|
||||
- `git status`: nothing unintended staged. Never commit `.agent_memory/`. If `docs/assets/screenshots/*.png` are dirty from a test run (not an intentional UI change), restore them: `git checkout -- docs/assets/screenshots`.
|
||||
|
||||
## 2. Body
|
||||
Draft per the SOP file (WHY-first summary, then changes under the 🌟 Features / 🛠️ Improvements / 🐛 Bug Fixes / 🧹 Chores categories that apply, **Testing Performed** section when tests were added/changed).
|
||||
|
||||
**Screenshots** (UI changes want them): commit the PNGs on the branch, push, then embed with commit-pinned raw URLs so they render in the PR body immediately:
|
||||
```
|
||||
https://raw.githubusercontent.com/<owner>/<repo>/<full-commit-sha>/<path/to/img.png>
|
||||
```
|
||||
Pin to the SHA that contains the image, not the branch name.
|
||||
|
||||
## 3. Push and open
|
||||
```bash
|
||||
git push -u origin HEAD
|
||||
gh pr create --draft --base <target> --title "<type>: <summary>" --body-file <(printf '%s' "$BODY")
|
||||
```
|
||||
- `--base` is the target branch chosen in pre-flight (`main` unless told otherwise) — always pass it explicitly; omitting it silently uses the repo default.
|
||||
- Draft by default; only `--ready` if explicitly asked.
|
||||
- End the body with: `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
|
||||
- Report the PR URL as a markdown link.
|
||||
9
.mcp.json
Normal file
9
.mcp.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$comment": "Team-shared MCP servers (auth-free only — anything needing tokens stays in user-level config). context7 = live library docs lookup; used constantly for Kotlin/Compose/AGP version questions.",
|
||||
"mcpServers": {
|
||||
"context7": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.context7.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user