| name | fetch-protos |
| description | Fetch latest protobuf definitions, verify build, summarize API changes, and scaffold new service stubs. Usage: /fetch-protos [flipcash|opencode] [commit_sha]
|
| user-invocable | true |
| argument-hint | [flipcash|opencode] [commit_sha] |
| allowed-tools | ["Bash","Read","Edit","Write","Glob","Grep","Agent"] |
Fetch Protos
Fetch protobuf definitions from upstream repos, verify they compile, summarize
API changes, and scaffold missing service layer implementations.
Pre-flight context
- Current proto files: !
find definitions/*/protos/src/main/proto -name "*.proto" 2>/dev/null | wc -l | tr -d ' ' proto files across targets
- Git status: !
git status --short definitions/
Input
Parse $ARGUMENTS to determine targets and optional commit SHA.
Rules:
- Known targets:
flipcash, opencode
- If no targets specified, fetch both (
flipcash and opencode)
- A hex string (7+ chars) as the last argument is treated as a commit SHA
- Examples:
/fetch-protos → fetch flipcash + opencode at HEAD
/fetch-protos flipcash → fetch flipcash only
/fetch-protos opencode abc1234 → fetch opencode at commit abc1234
/fetch-protos flipcash opencode → fetch both explicitly
Steps
Step 1 — Fetch protos
For each target, run the fetch script from the repo root:
bash scripts/fetch-protos.sh -t <target> [commit_sha]
Target-to-repo mapping (handled by the script):
| Target | Repository |
|---|
flipcash | git@github.com:code-payments/flipcash2-protobuf-api.git |
opencode | git@github.com:code-payments/ocp-protobuf-api.git |
Show the script output to the user.
Step 2 — Diff and summarize changes
Run git diff on the proto directories to identify what changed:
git diff --stat definitions/
git diff definitions/
For each changed .proto file, summarize:
- New RPCs added to services
- Modified RPCs (changed request/response types or fields)
- Removed RPCs
- New/modified messages and fields
Present a structured change summary table to the user. If nothing changed, report
that protos are already up to date and stop here.
Step 3 — Build verification
Build the definitions modules to verify the protos compile:
./gradlew :definitions:flipcash:models:assembleDebug :definitions:opencode:models:assembleDebug
Only build the targets that were fetched. If the build fails, show errors and stop.
Step 4 — Detect service layer impact
4a — RPC changes
For each new or modified RPC found in Step 2:
- Identify which service proto file it belongs to (e.g.,
account/v1/flipcash_account_service.proto)
- Search for the corresponding Api class in
services/<target>/src/**/network/api/
- Check if a method exists for the RPC
- If the RPC is new, check whether Service, Repository, and Controller layers also need updates
Present a report:
| RPC | Api | Service | Repository | Controller | Status |
|---|
NewRpc | missing | missing | missing | missing | New — needs scaffolding |
ModifiedRpc | exists | exists | exists | exists | Signature may need update |
4b — Message field changes (domain models)
For each message with added or removed fields (e.g., UserFlags, UserProfile):
- Search for the corresponding domain model in
services/<target>/src/**/models/
- Search for the corresponding mapper in
services/<target>/src/**/internal/domain/
- For added fields: add the property to the domain data class, set a sensible
default in the
Default companion, and map it in the mapper
- For removed fields: remove the property from the domain data class, its
default, and the mapper line
Common type mappings (match existing fields on the same model):
| Proto type | Domain type | Mapping |
|---|
uint64 amount/quarks fields | Fiat | Fiat(quarks = from.fieldName) |
bool | Boolean | from.fieldName |
string | String | from.fieldName |
int32/uint32 | Int | from.fieldName |
Duration | kotlin.time.Duration | from.fieldName.seconds.toDuration(DurationUnit.SECONDS) |
enum | sealed/enum domain type | from.fieldName.toDomain() (add private extension) |
When in doubt, look at how neighboring fields on the same message are typed and
mapped — follow the same pattern.
UserFlags-specific chain
When UserFlags fields change, the following files form a chain that must all be
updated together. Ask the user whether the new field should be read-only (display
only) or editable (overridable via the debug editor).
| # | File | What to update |
|---|
| 1 | services/flipcash/src/**/models/UserFlags.kt | Add/remove property + Default companion value |
| 2 | services/flipcash/src/**/internal/domain/UserFlagsMapper.kt | Add/remove mapping line in map() |
| 3 | apps/flipcash/shared/userflags/src/**/ResolvedUserFlags.kt | Add/remove ResolvedFlag<T> property + line in resolve() extension |
| 4 | apps/flipcash/features/userflags/src/**/internal/UserFlagsViewModel.kt | Add to readOnlyEntries (bool) or editableEntries() list |
If the field is editable (overridable), also update:
| # | File | What to update |
|---|
| 5 | apps/flipcash/shared/userflags/src/**/UserFlagsCoordinator.kt | Add FieldOverride<T> to Overrides data class + Overrides.None + overrides flow mapping |
| 6 | apps/flipcash/shared/userflags/src/**/Field.kt | Add data object subclass with preference key, encode/decode, label, editor |
| 7 | apps/flipcash/shared/userflags/src/main/res/values/strings.xml | Add label_flag_* (and hint_flag_* if needed) string resources |
For read-only fields (e.g., booleans like enablePhoneNumberSend):
- In
ResolvedUserFlags.resolve(), use FieldOverride.None (no override support)
- In
UserFlagsViewModel, add to readOnlyEntries with a string resource label
- No changes needed in
Overrides, Field.kt, or UserFlagsCoordinator
Present a report of domain model updates needed and apply them after user confirmation.
Step 5 — Scaffold new service stubs
For RPCs marked as needing scaffolding, ask the user if they want to scaffold them.
If confirmed, generate code following the patterns below.
Api method pattern
Location: services/<target>/src/main/kotlin/.../internal/network/api/<ServiceName>Api.kt
suspend fun newRpc(owner: KeyPair, ...): RpcServiceName.NewRpcResponse {
val request = RpcServiceName.NewRpcRequest.newBuilder()
.apply { setAuth(authenticate(owner)) }
.build()
request.validate().orThrow()
return withContext(Dispatchers.IO) {
api.newRpc(request)
}
}
Key conventions:
- Use
authenticate(owner) for Flipcash endpoints (returns Common.Auth)
- Use
sign(owner) for OpenCode endpoints (returns Model.Signature)
- Always call
request.validate().orThrow() before the RPC
- Always dispatch on
Dispatchers.IO
- Return raw proto response type
Service method pattern
Location: services/<target>/src/main/kotlin/.../internal/network/services/<ServiceName>Service.kt
suspend fun newRpc(owner: KeyPair, ...): Result<DomainType> {
return runCatching {
api.newRpc(owner, ...)
}.foldWithSuppression(
onSuccess = { response ->
when (response.result) {
RpcServiceName.NewRpcResponse.Result.OK -> Result.success()
RpcServiceName.NewRpcResponse.Result.DENIED -> Result.failure(NewRpcError.Denied())
RpcServiceName.NewRpcResponse.Result.UNRECOGNIZED -> Result.failure(NewRpcError.Unrecognized())
else -> Result.failure(NewRpcError.Other())
}
},
onFailure = { cause ->
Result.failure(cause.toValidationOrElse { NewRpcError.Other(cause = it) })
}
)
}
Error sealed class pattern
Location: services/<target>/src/main/kotlin/.../models/Errors.kt
sealed class NewRpcError(
override val message: String? = null,
override val cause: Throwable? = null
) : CodeServerError(message, cause) {
class Denied : NewRpcError("Denied")
class Unrecognized : NewRpcError("Unrecognized"), NotifiableError
data class Other(override val cause: Throwable? = null) : NewRpcError(message = cause?.message, cause = cause), NotifiableError
}
Add a subclass for each non-OK result enum value in the proto response. Mark
Unrecognized and Other with NotifiableError. Mark expected/benign errors
(e.g., NotFound, Denied) without NotifiableError.
Repository pattern
- Interface in
services/<target>/src/main/kotlin/.../repository/:
suspend fun newRpc(...): Result<DomainType>
- Internal impl in
services/<target>/src/main/kotlin/.../internal/repositories/:
override suspend fun newRpc(...): Result<DomainType> {
return service.newRpc(...)
.map { mapper.map(it) }
.onFailure { if (it !is NewRpcError.ExpectedCase) ErrorUtils.handleError(it) }
}
Controller pattern
Location: services/<target>/src/main/kotlin/.../controllers/<ServiceName>Controller.kt
suspend fun newRpc(...): Result<DomainType> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster"))
return repository.newRpc(owner, ...)
}
Hilt wiring
If a new Repository interface+impl pair was created, add a @Provides binding in
the corresponding Hilt module (FlipcashModule.kt or OpenCodeModule.kt).
Step 6 — Review and commit
Show the user a summary of all changes (proto updates + any scaffolded code).
Offer to commit with a conventional commit message:
chore(protos): update <target> protobuf definitions
If service stubs were also scaffolded, suggest a separate commit:
feat(<target>): scaffold service stubs for new RPCs
Never
- Edit generated protobuf code in
definitions/*/models/build/
- Commit without user approval
- Skip build verification
- Scaffold service code without asking the user first