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