前五篇讲了 Harness 的理论。这一篇用一个真实的 Android 开发场景——从零搭建一个登录模块的 Agent 开发环境——把七层全部落地。
一、项目技术栈
项目技术栈:Kotlin + Jetpack Compose + MVVM + Hilt + Coroutines + DataStore。
二、第一层 Instructions:告诉 AI "按什么规矩干"
2.1 CLAUDE.md(项目级规则)
这是整个 Harness 的入口,放在项目根目录:
<span># Android 登录模块开发规范</span>
<span>## 架构</span>
<span>-</span> 严格遵循 MVVM:Screen(UI)→ ViewModel → Repository → ApiService
<span>-</span> UI 只观察 ViewModel 的 StateFlow,不维护任何登录判断逻辑
<span>-</span> 依赖注入用 Hilt,禁止手动 new 对象
<span>## 代码风格</span>
<span>-</span> Kotlin,禁用 Java
<span>-</span> 用 Jetpack Compose,禁用 XML 布局
<span>-</span> 协程用 viewModelScope / lifecycleScope,禁止 GlobalScope
<span>-</span> 命名:状态用 UiState 后缀,事件用 UiEvent 后缀
<span>## 必须遵守</span>
<span>-</span> token 必须用 DataStore 持久化,禁止用内存变量
<span>-</span> 所有网络请求必须有错误处理(try/catch + Result 封装)
<span>-</span> 提交前必须通过:./gradlew assembleDebug + ./gradlew test + ktlint
<span>## 禁止</span>
<span>-</span> 禁止在 Activity/Fragment 里直接调用 ApiService
<span>-</span> 禁止用 SharedPreferences(用 DataStore 替代)
<span>-</span> 禁止硬编码字符串,所有文案放 strings.xml
<span>-</span> 禁止修改项目目录外的任何文件
设计要点: 每条规则都是可验证的。
2.2 Skill/login-dev/SKILL.md
把登录模块的特定规范写成Skill,AI 写代码时自动读到:
<span>---</span>
<span>name:</span> <span>login-dev</span>
<span>description:</span> <span>登录模块开发规范与</span> <span>8</span> <span>条验收标准。新增或修改登录页、token</span> <span>持久化、会话、登出等相关代码时使用。</span>
<span>---
</span>
<span># 登录模块开发规范(login-dev)</span>
<span>## 文件结构(必须生成)</span>
<span>-</span> <span>LoginUiState.kt:idle</span> <span>/</span> <span>loading</span> <span>/</span> <span>success</span> <span>/</span> <span>error</span> <span>四状态</span>
<span>-</span> <span>LoginViewModel.kt:暴露</span> <span>uiState:</span> <span>StateFlow<LoginUiState></span>
<span>-</span> <span>LoginScreen.kt:Compose</span> <span>UI,只观察</span> <span>uiState</span>
<span>-</span> <span>LoginRepository.kt:调用</span> <span>ApiService</span> <span>+</span> <span>DataStore</span>
<span>-</span> <span>LoginApiService.kt:Retrofit</span> <span>接口</span>
<span>-</span> <span>TokenManager.kt:DataStore</span> <span>封装,7</span> <span>天过期自动清除</span>
<span>## 验收标准(8 条,全部可验证)</span>
<span>①</span> <span>编译通过:./gradlew</span> <span>assembleDebug</span> <span>→</span> <span>BUILD</span> <span>SUCCESSFUL</span>
<span>②</span> <span>邮箱格式校验:空输入</span> <span>/</span> <span>格式错误有明确提示</span>
<span>③</span> <span>密码校验:最少</span> <span>6</span> <span>位</span>
<span>④</span> <span>token</span> <span>持久化:DataStore,重启</span> <span>App</span> <span>仍在</span>
<span>⑤</span> <span>token</span> <span>过期:7</span> <span>天后自动清除,跳转登录页</span>
<span>⑥</span> <span>状态正确:loading</span> <span>时按钮禁用,error</span> <span>时显示错误信息</span>
<span>⑦</span> <span>退出登录:清除</span> <span>token</span> <span>+</span> <span>重置状态</span>
<span>⑧</span> <span>单元测试通过:./gradlew</span> <span>test</span>
Skill 和 CLAUDE.md 的分工:CLAUDE.md 管项目级通用规则,Skill 管特定模块的具体规则。不要把所有东西塞进 CLAUDE.md,上下文越长模型越容易忽略后面的规则。
三、第二层 Knowledge:让 AI 记住之前干了什么
3.1 progress.md(每轮更新的进度文件)
<span># 登录模块开发进度</span>
<span>## 已完成</span>
<span>-</span> [x] 项目初始化(Compose + Hilt + Retrofit)
<span>-</span> [x] LoginApiService 接口定义
<span>-</span> [x] TokenManager(DataStore,7天过期)
<span>-</span> [x] LoginRepository
<span>-</span> [x] LoginViewModel(四状态)
<span>## 进行中</span>
<span>-</span> [ ] LoginScreen Compose UI
<span>## 待办</span>
<span>-</span> [ ] 单元测试(ViewModel + Repository)
<span>-</span> [ ] 集成测试(登录流程端到端)
<span>## 关键决策记录</span>
<span>-</span> 2026-08-22:选择 DataStore 而非 SharedPreferences
原因:DataStore 是 Kotlin 协程友好、线程安全、支持迁移
<span>-</span> 2026-08-22:token 过期策略定为 7 天
原因:业务需求,非安全最佳实践(生产环境应更短)
为什么需要这个: 对话上下文会溢出,跑到第 10 轮 AI 可能忘了第 3 轮做了什么。progress.md 是持久化记忆,每轮结束后 AI 自动更新,下一轮先读它再干活。
注意:光建这个文件,AI 不会自动去读(它不知道文件存在)。要让它每次开工前真读到,靠的是
SessionStartHook 自动注入;这里先知道"要有这么个文件",机制在第七节补全。
3.2 架构决策记录(ADR)
重大决策写进 docs/adr/,比如:
<span># ADR-001:选择 DataStore 而非 SharedPreferences</span>
<span>## 状态</span>
已接受
<span>## 背景</span>
登录模块需要持久化 token,SharedPreferences 和 DataStore 都能做。
<span>## 决策</span>
使用 Jetpack DataStore(Preferences DataStore)。
<span>## 理由</span>
<span>-</span> SharedPreferences 首次从磁盘加载(getSharedPreferences)和同步提交 commit() 会阻塞主线程,可能 ANR(apply() 的磁盘写虽在后台,但加载阶段仍有主线程阻塞隐患)
<span>-</span> DataStore 基于 Kotlin 协程,异步且线程安全
<span>-</span> DataStore 支持类型安全(Proto DataStore)
<span>-</span> 官方推荐替代 SharedPreferences
<span>## 后果</span>
<span>-</span> 最低 SDK 要求不变(DataStore 兼容 API 21+)
<span>-</span> 需要引入 androidx.datastore:datastore-preferences 依赖
ADR 让 AI(和人)知道"为什么这么选",而不是只看到"用了 DataStore"。交接靠文档,不靠嘴。
注意:和 progress.md 一样,光把 ADR 写进
docs/adr/,AI 也不会自动去翻。做法是差异化喂:progress.md 天天用,整篇注入;ADR 全文每次都注入太费 token、还会稀释注意力,所以 SessionStart Hook 只把"每篇 ADR 的标题索引"塞进上下文——AI 看到有过哪些决策、需要哪篇时再去读原文。
四、第三层 Tools:给 AI 几双"手"
4.1 这一层到底什么时候用
这层没有单独的配置文件,它从第一次说"按规范把登录模块写出来"那一刻就开始用了。 上一层 Instructions 是"告诉 AI 按什么规矩干",可规矩再清楚,AI 没有手也干不了活。Tools 层就是决定:AI 到底能动哪些东西——能读哪些文件、能改哪些文件、能跑哪些命令、能不能上网查资料。
在 Claude Code 里,这些"手"出厂就自带,不用新写工具,只需要知道给了它哪几双:
文件手:Read / Write / Edit
→ 谁来改 CLAUDE.md、progress.md、新建 Kotlin 文件?就是这双手
命令手:Bash
→ 跑 ./gradlew assembleDebug、<span>test</span>、ktlint,跑 adb、git,全靠它
→ 注意:这是一双<span>"万能手"</span>,能干好事也能闯祸,所以后面 7.x 要专门盯它
搜索手:Grep / Glob
→ 在项目里找代码,比如<span>"查一下全项目还有谁在用 SharedPreferences"</span>
查资料手:WebFetch / 浏览器
→ 查 Android 官方文档、Stack Overflow(可选,写错 API 时救场用)
扩展手:MCP(可选,本文不用)
→ GitHub MCP 建 PR 看 CI、Jira MCP 改 issue 状态
它就是这么用的:Read 看你的项目结构 → Write/Edit 新建 LoginViewModel.kt → Bash 跑 ./gradlew assembleDebug → Grep 自查有没有违规写法。这一整套动作,就是 Tools 层在工作。
4.2 这一层真正要做的两件事
第一件:别把手砍太狠。 如果你把文件手、命令手都禁了,AI 读不了代码、跑不了构建,那它再聪明也只能干聊。
第二件:把万能的 Bash 手盯住。 文件手只碰项目目录还好说,Bash 手却能跑任意命令——你没法从源头只给它"Gradle 手"而不给"删文件手",所以只能先用着,再用第七节的 Hook 在它按下回车前拦一道:
path-guard.sh(第7节):管文件手——Write/Edit 只能落在项目目录内;command-guard.sh(第7节):管命令手——跑gradlew clean、rm -rf这种直接拦下。
Tools 层 = AI 出厂带了哪几双手(别给多、也别砍没);
Hooks 层 = 这双手伸出去之前/之后,在旁边盯一眼、拦一下。
光靠 Tools 管不住 Bash,这正是本文要把 Hooks 单独拎成一层的原因。
五、第四层 Infrastructure:在哪干活
5.1 工作目录隔离
项目根目录:/Users/file/AndroidStudioProjects/LoginDemo/
→ AI 的工作目录就在这,新文件都落在这里
5.2 Android 环境
这些环境不是写进 Harness 配置的,而是电脑和项目里本来就有的开发环境。Harness 这层只负责确认一件事——AI 跑 ./gradlew 时,能不能在它的命令行里找到这些东西。
必需:
<span> -</span> JDK 17(Android Gradle Plugin 8.x 要求)
<span> -</span> Android SDK(compileSdk 34)
<span> -</span> Android Emulator(用于集成测试)
<span> -</span> Gradle 8.x(用 gradle wrapper,不依赖全局安装)
可选:
<span> -</span> LeakCanary(内存泄漏检测,debug 包)
六、第五层 Orchestration:谁干什么
6.1 三 Agent 分工
下面这三个"Agent",是同一个会话分几次扮演的三种身份:
生成(Developer):
→ 读 CLAUDE.md + login-dev 规范 → 写代码 → 跑编译
→ 只负责<span>"写出来"</span>,写完按 6.3 的格式交接
测试(Tester):
→ 只跑 ./gradlew <span>test</span>、ktlint,把客观结果列出来
→ 过了/没过、哪条挂了,如实报;不评价代码写得好不好
评审(Reviewer):
→ 对着 login-dev 里那 8 条验收标准,逐条读实际代码、看测试结果
→ 输出问题清单:[文件:行号] 问题 | 级别 → 证据
→ 不相信生成角色的自报,必须自己看代码
6.2 为什么必须分开
同一个 AI 又写又查:
→ <span>"我写的 token 持久化没问题吧?"</span> → <span>"嗯,看起来没问题"</span>
→ 假达标
分开后:
→ 生成 Agent:用了内存变量存 token
→ 评审 Agent:[TokenManager.kt:<span>15</span>] token 用 <span>var</span> 内存变量,重启丢失 | 严重
→ 真问题被发现
6.3 交接协议
生成 Agent 写完后,必须输出结构化的交接信息:
<span>## 本轮变更</span>
<span>-</span> 修改文件:LoginViewModel.kt, TokenManager.kt
<span>-</span> 新增文件:LoginUiState.kt
<span>-</span> 完成的验收项:①编译通过 ④token持久化
<span>-</span> 未完成:⑧单元测试
<span>-</span> 已知问题:无
评审 Agent 拿到这个交接信息,直接对照检查,不用从零猜"改了什么"。
6.4 这三个角色到底怎么真的跑起来
主会话自己当调度者,三个角色不是三个常驻进程,而是分几次派出去的任务。最朴素的做法:
第 1 句(切<span>"生成"</span>):
<span>"按 CLAUDE.md 和 .claude/skills/login-dev 的规范,把登录页写出来。
写完按'本轮变更'的格式告诉我:改了/新增哪些文件、哪几条验收过了。"</span>
→ 预期:它新建几个 .kt 文件,跑 ./gradlew assembleDebug 通过,最后打印 6.3 那段交接
第 2 句(切<span>"测试"</span>):
<span>"现在你是测试角色。只跑 ./gradlew test 和 ktlint,
把通过/失败的客观结果列出来,不要评价代码好坏。"</span>
→ 预期:它只贴测试结果,比如<span>"12/12 通过,ktlint 无告警"</span>,不聊别的
第 3 句(切<span>"评审"</span>):
<span>"现在你是评审角色。对着 login-dev 里那 8 条验收标准,
逐条读刚才的代码自己查,不信我上面的自报。
输出 [文件:行号] 问题清单,没有问题就说'8 条全过'。"</span>
→ 预期:它列出类似 [TokenManager.kt:15] token 用内存变量 | 严重 这样的清单
第 4 句(切回<span>"生成"</span>改):
<span>"把上面评审列的问题逐条改掉,改完再跑一次 ./gradlew test 确认。"</span>
→ 预期:它修代码,最后说<span>"问题已修,测试 12/12 通过"</span>
想省掉每次手动切,就把这段流程写进 CLAUDE.md。 在 CLAUDE.md 的"工作流程"里加这几行:
<span>## 工作流程(写 → 测 → 评 → 改)</span>
- 写完代码:按<span>"本轮变更"</span>格式汇报改了哪些文件、过了哪几条验收
- 测试角色:只跑 ./gradlew <span>test</span> 和 ktlint,报客观结果,不评价代码
- 评审角色:对着 login-dev 的 8 条标准逐条查代码,输出 [文件:行号] 问题清单
- 改完:把评审问题修掉,再跑一次 <span>test</span> 确认全过
本篇到此是"手动调度":流程是清楚的,但切换还得手动。现在先手动走几遍,才能看清每一步在交接什么。
七、第六层 Hooks:强制规则,模型无法绕过
这是 Android Harness 里最有价值的一层。CLAUDE.md 写了"禁止用 SharedPreferences",但模型可能忘了。Hook 让它想忘都忘不掉。
7.1 SessionStart:会话一启动,自动把 progress.md 喂给 AI
3.1 说了 progress.md 是"持久化记忆",但有个容易被忽略的坑:你只是在项目里建了这个文件,Claude Code 不会自己去读它——它根本不知道这个文件存在。光在规矩里写一句"开工前先读 progress.md",模型很可能忘。真正的硬办法,是用 SessionStart Hook:会话一打开,脚本自动把 progress.md 的全文读出来、再把 docs/adr/ 里每篇 ADR 的标题列成索引,一起通过 additionalContext 直接塞进上下文。这样 AI 一进来就看到进度、也知道做过哪些架构决策。
第 1 步:新建脚本 .claude/hooks/session-start/inject-progress.sh
<span>#!/bin/bash</span>
<span># .claude/hooks/session-start/inject-progress.sh</span>
<span># 会话一启动注入两样东西:</span>
<span># 1) progress.md 全文(当前进度,天天要用)</span>
<span># 2) docs/adr/ 的决策索引(只列标题清单;全文太占上下文,需要时再读那一篇)</span>
PROJECT_ROOT=<span>"<span>${CLAUDE_PROJECT_DIR:-.}</span>"</span>
OUT=<span>""</span>
<span># 1) 当前进度:全文注入</span>
PROGRESS=<span>"<span>$PROJECT_ROOT</span>/progress.md"</span>
<span>if</span> [[ -f <span>"<span>$PROGRESS</span>"</span> ]]; <span>then</span>
OUT+=<span>"【当前项目进度 progress.md,开工前先看,别重复已完成的工作】
<span>$(cat <span>"<span>$PROGRESS</span>"</span>)</span>
"</span>
<span>fi</span>
<span># 2) 架构决策 ADR:只列索引(每篇第一行标题),不塞全文</span>
ADR_DIR=<span>"<span>$PROJECT_ROOT</span>/docs/adr"</span>
<span>if</span> [[ -d <span>"<span>$ADR_DIR</span>"</span> ]]; <span>then</span>
INDEX=<span>""</span>
<span>for</span> f <span>in</span> <span>"<span>$ADR_DIR</span>"</span>/*.md; <span>do</span>
[[ -f <span>"<span>$f</span>"</span> ]] || <span>continue</span>
<span># macOS 的 BSD grep 没有 -m 选项,用 head -n1 取第一个标题行</span>
TITLE=$(grep <span>'^# '</span> <span>"<span>$f</span>"</span> | <span>head</span> -n1 | sed <span>'s/^# *//'</span>)
[[ -z <span>"<span>$TITLE</span>"</span> ]] && TITLE=<span>"<span>$(basename <span>"<span>$f</span>"</span>)</span>"</span>
INDEX+=<span>"- <span>$(basename <span>"<span>$f</span>"</span>)</span>:<span>${TITLE}</span>
"</span>
<span>done</span>
<span>if</span> [[ -n <span>"<span>$INDEX</span>"</span> ]]; <span>then</span>
OUT+=<span>"
【已记录的架构决策 docs/adr/:换存储/网络层/目录结构等,先查下面这些决策,别凭空推翻;要推翻就先改对应文件】
<span>$INDEX</span>"</span>
<span>fi</span>
<span>fi</span>
<span># 两样都没有就安静退出,不影响正常会话</span>
[[ -z <span>"<span>$OUT</span>"</span> ]] && <span>exit</span> 0
<span># --arg 把拼好的整段传给 jq,它会自动转义换行和引号</span>
jq -n --arg ctx <span>"<span>$OUT</span>"</span> <span>'{
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: $ctx
}
}'</span>
<span>exit</span> 0
第 2 步:给执行权限
<span>chmod</span> +x .claude/hooks/session-start/inject-progress.sh
第 3 步:注册到 settings.json
SessionStart 不是"调用某个工具"的事件,没有工具名可匹配,所以不需要 matcher,直接挂命令。在 .claude/settings.json 的 hooks 里加一段,和 PreToolUse、PostToolUse 平级:
<span>"SessionStart"</span>: [
{
<span>"hooks"</span>: [
{ <span>"type"</span>: <span>"command"</span>, <span>"command"</span>: <span>""</span>$CLAUDE_PROJECT_DI<span>R"/.claude/hooks/session-start/inject-progress.sh"</span> }
]
}
]
第 4 步:规矩里只管"写",不用管"读"
"读"已经被 Hook 强制了,CLAUDE.md 里只需要提醒 AI 更新。在 CLAUDE.md 的"工作流程"里加两句:
<span>## 工作流程</span>
- 每轮开始:progress.md 和 ADR 索引会被自动注入,直接按进度继续,别重做已完成的工作;涉及存储/网络层/目录结构这类决策时先查 docs/adr/,别凭空推翻
- 每轮结束:更新 progress.md(已完成 / 进行中 / 待办);做了新的重大决策就补一篇 docs/adr/
怎么验证生效: 重启 Claude Code、新开一个会话,先别急着下指令。正常情况下它在第一句回复里就会主动提到"根据 progress.md,当前做到……",说明进度真的被注入了。如果它完全不知道之前的进度,说明 Hook 没注册或脚本没执行权限——回去查 settings.json 里那段路径对不对、脚本 chmod +x 没有。
闭环一句话:读 progress.md 用 SessionStart Hook 强制(不靠自觉),写 progress.md 用 CLAUDE.md 提醒(一个轻量动作) 。这才是"持久化记忆"真正生效的样子。
八、第七层 Observability:能看见发生了什么
8.1 构建日志:AI 跑的命令
做法一:写进 CLAUDE.md,让 AI 跑命令时自己带 tee
AI 是通过 Bash 敲命令的,它敲什么命令是可以被你约束的。在 CLAUDE.md 里加一句规矩:
<span>## 留档要求</span>
- 每次跑 ./gradlew assembleDebug / <span>test</span>,命令必须带 <span>tee</span> 留档:
./gradlew assembleDebug 2>&1 | <span>tee</span> logs/build-$(<span>date</span> +%Y%m%d-%H%M%S).<span>log</span>
- 交接信息里必须附上本次日志的文件名,别只说<span>"构建通过了"</span>
做法二:以后挂进 PostToolUse Hook,无人值守留档
等你以后真把"改完 .kt 自动跑完整构建"挂进 PostToolUse,那个 Hook 脚本里跑构建时同样加 | tee logs/...,就实现了"AI 改完代码、都不用开口,日志自动落盘"。。
========== 构建记录 ==========
时间:2026-08-22 14:30:00
触发:你让 AI 跑构建(命令由 AI 通过 Bash 执行)
命令:./gradlew assembleDebug 2>&1 | <span>tee</span> logs/build-20260822-143000.<span>log</span>
结果:BUILD SUCCESSFUL
耗时:42 秒
警告:2 个未使用的 import
===============================
8.2 测试报告:和构建一样,也 tee 留档
测试和构建是同一个道理:./gradlew test 也是 AI 通过 Bash 跑的,输出贴在对话里会滚走。所以 CLAUDE.md 那条"留档要求"对它同样生效——跑 test 时也带 tee:
落盘文件长这样:
========== 测试报告 ==========
时间:2026-08-22 14:35:00
命令:./gradlew <span>test</span> 2>&1 | <span>tee</span> logs/test-20260822-143500.<span>log</span>
通过:12/12
失败:0
失败详情:无
覆盖率(需配 JaCoCo 才有,没配就删掉这两行):
LoginViewModel 85%,TokenManager 92%
===============================
九、完整 Harness 配置后的效果
配置完七层、并把 6.4 的"写→测→评→改"工作流程写进 CLAUDE.md 后,你在同一个对话窗口里只说一句"按工作流程来",流程就会按下面跑(下面的"生成/测试/评审"是同一个会话轮换的身份,):
你:按规范开发登录模块(或:按工作流程来)
↓
① 生成角色:
SessionStart 已自动喂入 CLAUDE.md + Skill + progress.md → 知道规矩和进度
写代码 → PostToolUse Hook 自动跑 ktlint → 通过
跑 assembleDebug → 通过
输出<span>"本轮变更"</span>交接信息
↓
② 测试角色:
只跑 ./gradlew <span>test</span> → 12/12 通过
如实输出测试报告,不评价代码
↓
③ 评审角色:
对着 8 条验收标准逐条读代码
输出 [文件:行号] 问题清单(或<span>"8 条全过"</span>)
↓
④ 生成角色(改):
把评审列的问题逐条修掉
改完再跑一次 ./gradlew <span>test</span> 确认全过
↓
你:回来收结果,看交接信息、日志和测试报告
你不在的每一分钟,Harness 在替你盯着: 规则有没有遵守、代码有没有编译、测试有没有过、有没有碰项目外的文件。
十、深度补充:Android Harness 的进阶配置
10.1 完整的 CLAUDE.md 示例
前面给的是简化版,这里是一个生产级 Android 项目的完整 CLAUDE.md:
<span># Android 项目开发规范</span>
<span>## 项目信息</span>
<span>-</span> 项目名:LoginDemo
<span>-</span> 包名:com.example.logindemo
<span>-</span> minSdk:24,targetSdk:34,compileSdk:34
<span>-</span> 语言:Kotlin 1.9+
<span>-</span> UI:Jetpack Compose BOM 2024.09.00
<span>-</span> 架构:MVVM + Clean Architecture(app / core-data / core-domain)
<span>-</span> DI:Hilt 2.48+
<span>-</span> 异步:Kotlin Coroutines + Flow
<span>-</span> 网络:Retrofit 2.9+ + OkHttp 4.12+
<span>-</span> 存储:DataStore Preferences 1.0+
<span>-</span> 测试:JUnit 4 + MockK + Turbine(Flow测试)+ Compose UI Test
<span>## 必须遵守的规则</span>
<span>1.</span> 所有新代码必须用 Kotlin,禁止新增 Java 文件
<span>2.</span> UI 只用 Jetpack Compose,禁止新增 XML 布局
<span>3.</span> 依赖注入只用 Hilt,禁止手动管理依赖图
<span>4.</span> 异步只用 Coroutines/Flow,禁止 RxJava / AsyncTask
<span>5.</span> 本地存储只用 DataStore / Room,禁止 SharedPreferences
<span>6.</span> 网络请求必须封装 Result<span><span><<span>T</span>></span></span>,禁止直接抛异常到 UI 层
<span>7.</span> 每个 ViewModel 必须暴露 StateFlow<span><span><<span>UiState</span>></span></span>,禁止用 LiveData(新项目)
<span>8.</span> 字符串必须放 strings.xml,禁止硬编码
<span>9.</span> 颜色必须放 color.xml / Theme,禁止硬编码色值
<span>10.</span> 提交前必须通过:assembleDebug + test + ktlintCheck
<span>## 禁止事项</span>
<span>-</span> 禁止在 Activity/Fragment/Composable 中直接调用 ApiService 或 Repository
<span>-</span> 禁止用 GlobalScope,必须用 viewModelScope / lifecycleScope / rememberCoroutineScope
<span>-</span> 禁止在主线程执行网络/数据库操作
<span>-</span> 禁止修改 .gitignore 中忽略的文件
<span>-</span> 禁止执行 gradlew clean(会清除构建缓存)
<span>-</span> 禁止操作项目目录外的任何文件
<span>-</span> 禁止提交包含 TODO/FIXME 的代码(除非关联了 issue)
<span>## 代码风格</span>
<span>-</span> 函数名用动词开头(loadUser, submitLogin)
<span>-</span> 状态类用 UiState 后缀,事件用 UiEvent 后缀
<span>-</span> 密封类/枚举用大写开头,常量用大写+下划线
<span>-</span> 单文件不超过 300 行,单函数不超过 50 行
<span>-</span> 超过 3 个参数用 data class 封装
<span>## 测试要求</span>
<span>-</span> ViewModel 必须有单元测试(状态流转 + 异常处理)
<span>-</span> Repository 必须有 mock 测试
<span>-</span> 核心 UI 必须有 Compose UI 测试
<span>-</span> 测试命名:<span>`方法名_场景_预期结果`</span>(如 <span>`login_invalidPassword_showsError`</span>)
<span>## 工作流程</span>
<span>-</span> 每轮开始:progress.md 与 docs/adr/ 索引已由 SessionStart Hook 自动注入,直接接着做,不要重复已完成的工作
<span>-</span> 涉及已记录决策(存储/网络/目录结构等)时,先查 docs/adr/ 对应那篇,不要凭空推翻;要改决策就先改 ADR 文件
<span>-</span> 每轮结束:更新 progress.md(已完成 / 进行中 / 待办);做了新的重大决策就补一篇 docs/adr/
<span>## Git 规范</span>
<span>-</span> 分支名:feature/登录模块 / bugfix/token过期
<span>-</span> Commit message:<span>`<type>: <描述>`</span>(type: feat/fix/refactor/test/docs)
<span>-</span> 每个 Commit 只做一件事,禁止"大杂烩"Commit
<span>-</span> PR 必须关联 issue,描述包含:变更内容、测试方式、截图(UI变更)
上面的版本号(compileSdk/targetSdk 34、Compose BOM 2024.09、Hilt 2.48、Retrofit/OkHttp 等)只是写作时的示例。新建项目时以 Android Studio 当前稳定版和你项目里实际在用的版本为准,不要照抄这些旧版本号;规则和目录结构才是要复用的部分。
10.2 完整的 Hook 脚本
10.3 Android 特定的 Harness 配置
前面第七节讲了三个通用 Hook(路径白名单、禁 clean、自动 ktlint)。Android 项目还有几个特有的坑,
10.3.1 构建变体:禁止 AI 碰 release
问题: Android 项目有 debug 和 release 两个变体。release 要签名密钥、开混淆,AI 不知道密钥在哪,也不应该知道。如果 AI 不小心跑了 ./gradlew assembleRelease,要么失败报错,要么打出一个没签名的包。
怎么配(两层:CLAUDE.md 提醒 + Hook 强制):.claude/hooks/pre-tool-use/release-guard.sh
10.3.2 ProGuard/R8:改了 build.gradle 就提醒检查
问题: Android release 开了 R8 混淆后,AI 新加一个用了反射的类,release 包就崩(ClassNotFoundException)。但 AI 改 build.gradle 的时候不会主动想"要不要加 ProGuard 规则"。
怎么配(两层:CLAUDE.md 提醒 + Hook 自动提醒): .claude/hooks/post-tool-use/gradle-change-watch.sh
10.3.3 依赖版本统一:从根上减少冲突
问题: 两个依赖间接引入了不同版本的 okhttp 或 coroutines,编译时没报错,运行时崩了(NoSuchMethodError)。AI 新加依赖时直接写死版本号,很容易和已有版本冲突。
怎么配(在 build.gradle 里配置版本 + CLAUDE.md 提醒):
10.3.4 全部配完后的完整项目目录
到这里本篇讲的所有东西都落地了。把整张图拼起来,你的项目根目录 LoginDemo/ 应该长这样(前面零散讲的文件一次看全):
LoginDemo/
├── CLAUDE.md <span># 2.1/10.1 项目级开发规范</span>
├── progress.md <span># 3.1 当前进度(由 7.1 启动时注入)</span>
├── build.gradle <span># 10.3.3 根目录 ext{} 统一版本</span>
├── settings.gradle
├── local.properties <span># 5.3 记 sdk.dir</span>
│
├── docs/
│ └── adr/ <span># 3.2 架构决策(7.1 只注入标题索引)</span>
│ └── ADR-<span>001</span>-datastore.md
│
├── .claude/ <span># Harness 的家,Claude Code 专门读这里</span>
│ ├── settings.json <span># 注册全部 Hook(10.3.1 那张完整版)</span>
│ ├── hooks/
│ │ ├── pre-tool-<span>use</span>/
│ │ │ ├── path-guard.sh <span># 7 路径白名单</span>
│ │ │ ├── command-guard.sh <span># 7 禁 clean / 危险命令</span>
│ │ │ └── release-guard.sh <span># 10.3.1 禁 release 构建</span>
│ │ ├── session-start/
│ │ │ └── inject-progress.sh <span># 7.1 启动注入 progress 全文 + ADR 索引</span>
│ │ └── post-tool-<span>use</span>/
│ │ ├── auto-ktlint.sh <span># 7 改完 .kt 单文件跑 ktlint</span>
│ │ └── gradle-change-watch.sh <span># 10.3.2 改 gradle 提醒 ProGuard</span>
│ └── skills/
│ └── login-dev/
│ └── SKILL.md <span># 2.2 登录模块领域规则</span>
│
├── app/ <span># Android 工程本身</span>
│ ├── build.gradle
│ └── src/main/java/com/example/logindemo/
│ ├── LoginScreen.kt
│ ├── LoginViewModel.kt
│ ├── LoginRepository.kt
│ ├── LoginApiService.kt
│ └── TokenManager.kt
│
├── .github/
│ └── workflows/
│ └── android-ci.yml <span># 10.4 云端质量门</span>
│
└── logs/ <span># 8.1 构建/测试日志(跑构建时 tee 出来)</span>
读图三句话:
- 根目录放"给人看的规矩":CLAUDE.md、progress.md、docs/adr;
.claude/放"给 AI 强制执行的":settings.json 注册 + hooks 脚本 + skills;app/、.github/、logs/是工程本身和它的云端/留档。
CLAUDE.md 和 build.gradle 在项目根目录,不在 .claude/ 里;每个 .sh 都要 chmod +x。
10.4 CI/CD 集成:让 Harness 在云端也跑
在本地写代码,PostToolUse Hook 会在你每次改完文件后自动跑 ktlint、编译检查——这就像你写完作业立刻自己检查一遍错别字。但它有三个漏洞:
- Hook 只盯你配好的那些工具和路径:AI 要是换种方式写文件(比如直接用 Bash 写,而不是走 Edit/Write),或者改了 Hook 没覆盖到的文件,这次检查根本不触发;更要命的是,那些只写在规矩里、没真配成 Hook 的检查,全靠 AI 自觉——它嘴上说"跑过了,没问题",你不亲眼看到终端输出就没法确认;
- 你本机的环境可能被自己改乱了(SDK 版本、缓存、本地配置),"在我这儿能跑"不代表别人那儿也能跑;
- 代码 push 上去、别人要合并时,你没法保证对方电脑上 Hook 是开着的。
CI/CD 就是一台干净的、在云端的电脑:一旦有人提交代码,它自动拉最新代码、从零配好环境、把你本地那套检查命令原封不动再跑一遍。它不认识你、不会被 AI 哄、环境是全新的——它说绿,才是真的绿。
所谓"Harness 在云端也跑",不是把 .claude/ 文件夹搬到云上,而是:本地 Hook 和 CI 用的是同一套检查规则(同样的 ktlint 规则、同样的 test、同样的 assembleDebug),只是一个在你改代码时即时跑轻量版、一个在代码合并前无人值守地跑全量版。标准只写一份,两边共用。
一句话:Hook 让你写代码时少犯错,CI 保证代码出门时真的没问题;两边用的是同一套检查规则,只是轻重不同,所以标准永远一致。
十二、这一篇总结
<span>1.</span> Android Harness 七层落地:
Instructions → CLAUDE.md + Skill(可验证的规则)
Knowledge → progress.md + ADR(持久化记忆)
Tools → 自带的读写/命令/搜索手,危险操作靠 Hook 盯
Infrastructure → 工作目录隔离 + Android SDK 环境
Orchestration → 生成/测试/评审三 Agent 分离
Hooks → 路径白名单 + 命令守卫 + 自动 ktlint
Observability → 构建日志 + 测试报告 + 成本追踪
<span>2.</span> 核心原则:
规则要可验证,不要形容词
硬规则用 Hook,不要靠 CLAUDE.md
Hook 要轻量,不要拖死 Agent
记忆要持久化,不要靠对话上下文
<span>3.</span> 效果:你不在的时候,Harness 替你盯着规矩、编译、测试、安全边界
本文提供了一套完整的Android Agent开发环境搭建指南,实用性强,尤其适合使用Claude Code等工具进行AI辅助开发的团队,能显著提升代码规范性和流程可靠性。