469 Commits

Author SHA1 Message Date
19767c4284 更新skill 2026-07-20 10:56:26 +09:00
7dae5f2bf8 移除脚本输出 2026-07-20 09:30:21 +08:00
4766fed174 删除例子 2026-07-20 09:29:42 +08:00
d022cc8788 迭代方案确认 2026-07-17 16:39:41 +08:00
bcf3e31db6 迭代计划准备 2026-07-16 15:08:07 +08:00
c4f430ccb3 迭代计划准备 2026-07-16 15:07:59 +08:00
1a9db9328e 批量购买 2026-07-15 12:00:05 +08:00
2e130b98f5 临时备份一次 2026-07-13 12:01:18 +09:00
5cdcdad534 Create 业务需求.md 2026-07-11 15:10:24 +08:00
d2e08dbbec skill提交
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m22s
2026-07-11 15:32:56 +09:00
026d4908d8 删除
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 53s
2026-07-11 12:28:38 +09:00
31232ea899 优化迁移脚本速度
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 53s
2026-07-10 13:10:00 +09:00
b38df737e1 先短暂去除限制,上传迁移脚本
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m51s
2026-07-09 18:23:29 +09:00
346156ee9b 卡只允许支付宝支付,设备只允许微信支付,钱包充值也遵循这个规则
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-07-03 10:26:48 +09:00
0d79130e07 入参
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m46s
2026-07-02 17:02:13 +09:00
b3fb8c7a82 资产详情新增两个字段
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-07-02 16:49:50 +09:00
44fb21eb6a 修复导入重试的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-07-02 15:13:22 +09:00
8f738ffbe8 导入的问题修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m51s
2026-07-02 13:04:36 +09:00
6db152bb2f chore: 安装 ponytail lazy senior dev 规则 2026-07-01 12:26:19 +09:00
fc6af43baa 修复:溢出流量应优先记到主套餐而非加油包
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m47s
主套餐和加油包同时 Depleted 时,recordOverflowToDepletedPackage
原先取 id 最大的套餐,可能取到加油包,语义不对。

改为优先查 master_usage_id IS NULL(主套餐),找不到时
再回退取任意 Depleted 套餐。
2026-07-01 12:06:19 +09:00
52bdbbae25 修复:套餐耗尽后流量详单断档问题
问题:套餐 status=Depleted 后,queryActivePackages 查不到套餐,
DeductDataUsage 直接 return CodeNoAvailablePackage,导致上游
仍有真实流量时,tb_package_usage_daily_record 不再写入,详单断档。

修复:无 Active 套餐时,找最近一条 Depleted 套餐,将溢出流量
累加到 data_usage_mb 并写入当日 daily_record,保持详单连续性。
status 不变,不重复触发停机。
2026-07-01 12:03:39 +09:00
76f657c986 1. 套餐过期前再主动同步一次流量
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
2. 生效套餐逻辑错误的问题
2026-06-30 15:54:55 +09:00
a2793f7bec 需要改造的地方 2026-06-29 18:17:06 +09:00
b5b7f9a41e 迁移
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-06-29 12:29:31 +09:00
b930662817 从乐观锁变成悲观锁 2026-06-29 12:29:21 +09:00
ab9da7bb33 修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
2026-06-26 13:11:39 +09:00
781e82441d 环境变量
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 39s
2026-06-26 11:59:28 +08:00
f4b6a55b27 驳回
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 7m17s
2026-06-26 12:12:15 +09:00
8cf921f11c 驳回接口
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
2026-06-25 17:10:24 +09:00
c5871b59a1 修复开放接口 wallet/package-orders 支持 IMEI 下单
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
2026-06-23 11:46:02 +09:00
c0b9604515 完成
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-06-23 11:05:58 +09:00
5c2ef97c24 相关问题优化以及新功能开发
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
迁移 155- 157
2026-06-22 17:21:13 +09:00
2885b503b3 一次重构修复 2026-06-22 12:15:40 +09:00
062f5a436f Merge branch 'main' of https://git.boss160.cn/csxj2026/junhong_cmp_fiber
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m17s
2026-06-22 11:10:58 +08:00
ba435dd6a6 关于绑定的问题
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-06-22 12:08:41 +09:00
3f21f7a7d6 让超时时间设置成60秒 2026-06-21 17:06:40 +08:00
5f960daf78 修改过期时间跟修改流量
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m10s
2026-06-18 15:53:43 +09:00
3b7b856e48 企业授权增强与资产列表扩展
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m9s
- 企业卡授权唯一约束:新增 DB 迁移(000154),卡级部分唯一索引防止同一张卡被多个企业同时持有,Service 层新增跨企业冲突检测
- 单卡列表新增 network_status 过滤参数
- 单卡/设备列表新增 asset_status、asset_status_name、generation 响应字段
- 单卡/设备列表新增企业维度过滤(authorized_enterprise_id、is_authorized_to_enterprise)及响应中企业授权信息(批量加载,无 N+1)
- 主钱包流水/退款列表新增 asset_identifier 精确过滤参数
- 企业卡授权/收回接口升级为三模式(list/range/filter),企业设备授权/收回升级为双模式(list/filter)
- 升级 sonic v1.14.2 → v1.15.2 以兼容 Go 1.26

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 16:23:03 +09:00
cf36f1447f 尝试某个skills
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m24s
2026-06-16 18:58:55 +09:00
Break
1f634eb465 init
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s
2026-06-16 15:15:50 +08:00
Break
84ab3bad99 order导出
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 3m11s
2026-06-16 09:38:20 +08:00
Break
6c8594633a 设备导出
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
2026-06-15 17:29:36 +08:00
Break
064961471b 设备导出
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m42s
2026-06-15 16:43:50 +08:00
Break
e37aad9e1d 修复导出数据不全的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m41s
2026-06-15 15:55:32 +08:00
Break
7ec84fbc0f 导出系统
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m50s
2026-06-15 15:28:29 +08:00
Break
2f0b24ce88 迁移脚本的修复 2026-06-15 10:53:47 +08:00
Break
e56649e5be 还有例子
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 53s
2026-06-12 18:10:37 +08:00
Break
1c492fe8db 更新一版 2026-06-12 18:10:22 +08:00
Break
e139f5e227 管理员也可以
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m42s
2026-06-12 17:58:25 +08:00
Break
016e7bee79 修改过期时间,以及修改套餐已用量
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-06-12 10:41:24 +08:00
Break
c437593a8c 修复错误购买报错的问题,修复接口返回虚流量启动错误的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
2026-06-11 14:51:26 +08:00
Break
134021c91e 修复错误的停止
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m51s
2026-06-11 10:01:35 +08:00
Break
5c779cb6e0 不允许购买第二个主套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m50s
2026-06-10 15:24:26 +08:00
Break
32c9859b38 监控 2026-06-10 01:15:26 +08:00
Break
283e3e3eb3 迁移脚本的一些修复 2026-06-09 11:29:40 +08:00
Break
6a70713b30 加分布式锁
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m0s
2026-06-09 10:54:13 +08:00
Break
4ccbf13197 开放接口已用完流量查询错误的问题 2026-06-08 10:10:10 +08:00
Break
0ae8148689 审批金额
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m13s
2026-06-06 14:55:48 +08:00
Break
9a802c04e5 审计日志修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m20s
2026-06-05 17:22:05 +08:00
Break
4d419a1966 校验bug 2026-06-05 16:59:27 +08:00
Break
fe2041bcf5 机卡分离复机判断
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m13s
2026-06-05 14:20:58 +08:00
Break
6ac19d6565 支撑单卡
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
2026-06-04 14:29:20 +08:00
Break
cce27975a1 设备关键词查询
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
2026-06-04 10:34:58 +08:00
Break
d86ac46761 关键词查询
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-06-04 10:34:14 +08:00
Break
420f3d83f9 应当允许卡就算没有销售也能换货
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-06-04 10:26:47 +08:00
Break
27fba9160c 提案
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 56s
2026-06-04 09:39:20 +08:00
Break
5089a71764 返回错误原因
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m25s
2026-06-03 17:52:51 +08:00
Break
5f57429fb0 直接换货流程
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m28s
2026-06-03 16:55:32 +08:00
Break
46ede81aef 修复佣金错误计算的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m15s
2026-06-03 10:25:49 +08:00
Break
36e68e6672 提案 2026-06-02 16:56:18 +08:00
Break
a6cd550b94 1
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m18s
2026-06-02 16:56:09 +08:00
Break
c6e65fde9b 开放接口 2026-06-02 16:56:01 +08:00
Break
1b47fb8f32 佣金计算问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m23s
2026-06-02 16:19:42 +08:00
Break
c5126c583b 修复以前的老数据
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m51s
2026-06-01 11:49:24 +08:00
Break
918b9b4f25 退款也应该记录快照 2026-06-01 11:45:31 +08:00
Break
944526d9ef 修复订单金额落库不对的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m17s
2026-06-01 11:21:29 +08:00
Break
6c8352491c 修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 53s
2026-05-29 15:08:25 +08:00
Break
1ee554daec 钱包流水新增资产信息
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m59s
2026-05-29 10:52:12 +08:00
Break
34a7cf0f3c 修复一些问题 2026-05-29 10:30:19 +08:00
Break
6a3fffc46c 错误的套餐购买限制
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
2026-05-29 09:44:48 +08:00
Break
872769e69e 迁移脚本的一些问题修复 2026-05-28 17:22:52 +08:00
Break
1550ef3cb1 修复超额流量不记录的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m18s
2026-05-28 15:19:54 +08:00
Break
58863e8ec5 数据迁移脚本
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 54s
2026-05-27 11:10:56 +08:00
Break
0d96a94e5f 关于上游流量同步覆盖修复,以及新增redis同步迁移
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m25s
2026-05-27 10:59:19 +08:00
Break
cf689beceb 修复问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m9s
2026-05-26 18:06:51 +08:00
Break
f4d92f99a2 新增退款凭证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m41s
2026-05-26 17:16:11 +08:00
Break
e131d6ba77 imei
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 21m51s
2026-05-26 16:55:35 +08:00
Break
bcbc290bad 卡新增imei字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m10s
2026-05-26 16:30:12 +08:00
4c1e4f428a 一次补偿
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m2s
2026-05-25 14:40:34 +08:00
20b5d70af9 1. 清理预充值SQL
2. 查日志的脚本
2026-05-25 14:30:09 +08:00
226474b434 轮训有问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
2026-05-22 16:21:46 +08:00
860f589b9a 错误提示有问题 2026-05-22 15:54:54 +08:00
2adbc87a52 Merge commit 'a03ba5d39645948f1c48ca0cfccc74c6edd1ccbf'
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m14s
2026-05-22 14:20:21 +08:00
6e564d1d1a Merge commit '599289a94ee0b8f36a09711122303a5bfacb265a' 2026-05-22 14:20:21 +08:00
3d28e29eaa Merge commit '58bdb2f18e5dd4eef84467961a1b55f3434b9ee5' 2026-05-22 14:20:20 +08:00
0948494b1c task: 修复支付宝C端入口构建失败
移除不存在的 alipay.CalcExpireAt 调用,改为按 cfg.AliPayExpireMinutes(默认 30 分钟)
内联计算 expireAt;将所有 BuildWapPayURL 调用从 5 参数改为 4 参数(移除 returnURL)。
修复范围:getOrBuildAlipayPaymentLink、createAlipayForceRechargeOrder、createAlipayRecharge。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:18:21 +08:00
599289a94e task: 修复支付宝C端入口构建失败
移除不存在的 alipay.CalcExpireAt 调用,改为按 cfg.AliPayExpireMinutes(默认 30 分钟)
内联计算 expireAt;将所有 BuildWapPayURL 调用从 5 参数改为 4 参数(移除 returnURL)。
修复范围:getOrBuildAlipayPaymentLink、createAlipayForceRechargeOrder、createAlipayRecharge。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:13:46 +08:00
b988f99a95 task: 完成支付宝回调安全(验签、多维度校验、业务分发) 2026-05-22 14:07:20 +08:00
56adc30409 task: 完成支付宝C端入口
新增支付宝支付方式支持:
- DTO:ClientPaymentLink、ClientPayOrderRequest/Response 增加 alipay;ClientCreateOrderRequest 增加 payment_method;ClientCreateOrderResponse 增加 payment_link;ClientCreateRechargeRequest 允许 alipay,app_type 改为微信必填;ClientRechargeResponse PayConfig 改为可选指针,增加 payment_link
- Service(client_order):PayOrder 新增 alipay 分支(不需要 app_type/OpenID,复用或新建 pending payment,生成 WAP URL);CreateOrder 强充路径按 payment_method 分支,alipay 不需要 app_type/OpenID;幂等命中时支付宝强充返回同一充值单对应的 payment_link;新增 getOrBuildAlipayPaymentLink(过期 payment 自动续建);新增 createAlipayForceRechargeOrder(事务创建充值单+支付单,WAP URL 生成失败则标记 failed)
- Handler(client_wallet):CreateRecharge 移除微信唯一限制,按 payment_method 分发到 createWechatRecharge / createAlipayRecharge;alipay 分支事务内创建充值单+支付单,然后生成 WAP URL,失败标记 payment failed
- 依赖 worker-1 共享 API:pkg/alipay.BuildWapPayURL/CalcExpireAt、Payment.ExpireAt、WechatConfig.AliReturnURL/AliPayExpireMinutes、PaymentStore.FindLatestPendingByOrderAndMethod;本 worktree 暂不能独立构建,待 leader 集成

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:07:20 +08:00
53e95751b1 task: 完成支付宝共享基础(迁移/WechatConfig/PaymentStore/pkg/alipay) 2026-05-22 14:07:20 +08:00
58bdb2f18e task: 完成支付宝共享基础(迁移/WechatConfig/PaymentStore/pkg/alipay) 2026-05-22 12:09:55 +08:00
a03ba5d396 task: 完成支付宝回调安全(验签、多维度校验、业务分发) 2026-05-22 12:09:50 +08:00
2768deb0b6 设备新增搜索条件
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m8s
2026-05-20 10:27:30 +08:00
e2301d5f3b chore(集成验证): gofmt 修复 DTO 格式并重新生成 OpenAPI 文档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s
- 修复 ListStandaloneIotCardRequest 和 ListDeviceRequest 字段对齐格式
- 重新生成 docs/admin-openapi.yaml,两个列表接口均新增 has_active_package 参数
- go build ./cmd/api、go build ./cmd/worker 均通过验证

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 12:15:06 +08:00
9f5c5c3680 feat(卡列表): 新增 has_active_package 查询条件筛选生效中套餐
- DTO ListStandaloneIotCardRequest 增加 HasActivePackage *bool 字段,
  query/json 名 has_active_package,description 中文
- Service ListStandalone 将 HasActivePackage 映射为 filters["has_active_package"]
- Store applyStandaloneFilters 新增 EXISTS/NOT EXISTS 子查询,
  使用 constants.PackageUsageStatusActive,限制 master_usage_id IS NULL
  且 deleted_at IS NULL,所有查询路径(默认/两阶段/并行)均复用此函数

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 12:15:06 +08:00
5e7e68cce7 feat(设备列表): 新增 has_active_package 查询条件
- DTO ListDeviceRequest 增加 HasActivePackage *bool 字段(query/json: has_active_package)
- Service List 方法将 HasActivePackage 透传至 filters["has_active_package"]
- Store applyDeviceFilters 新增 applyHasActivePackageFilter:
  true  → EXISTS 子查询(生效中主套餐)
  false → NOT EXISTS 子查询
  均限制 master_usage_id IS NULL、deleted_at IS NULL,状态使用 constants.PackageUsageStatusActive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 12:15:06 +08:00
ca939ff617 暂存一下 2026-05-19 11:57:02 +08:00
8de9c59b1c 查询
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m13s
2026-05-19 11:32:41 +08:00
631c5392b6 1
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m56s
2026-05-18 10:47:10 +08:00
af5a21ed1e 新增三个同步时间字段 2026-05-18 10:47:07 +08:00
480d4c4583 1
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m59s
2026-05-18 10:34:04 +08:00
ed071918ed 钱包订单退款时应当正确退回,充值时允许最低充值1分
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
2026-05-18 09:51:59 +08:00
d597a25c69 机卡分离复机接口
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m1s
2026-05-14 09:50:48 +08:00
81aa3c2b11 新增字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m15s
2026-05-13 17:50:21 +08:00
a5388446d3 新增字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
2026-05-12 14:10:48 +08:00
8f3a68a673 新增上游的停机原因
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m5s
2026-05-12 11:13:04 +08:00
95fc0b0a1b 修复一些问题,主要是生效套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s
2026-05-12 10:32:38 +08:00
b7369a9c71 修复并发,暂时的,不够完整,后续还是需要重新设计,这个太乱了,傻逼AI
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m22s
2026-05-11 17:19:40 +08:00
05a5694a5f 修复bug
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m5s
2026-05-11 16:45:37 +08:00
1350a9ed79 批量
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-05-11 14:54:23 +08:00
93200a9074 开放接口完成
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
2026-05-11 14:26:10 +08:00
02d522564f 删除hurl 2026-05-11 12:00:02 +08:00
b6d21b81e3 文档生成 2026-05-11 11:59:36 +08:00
98ff88d5c3 开放接口,修复上游同步不对的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
2026-05-11 11:20:48 +08:00
a9eaf1d697 修复缓存没有更新的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m13s
2026-05-11 10:38:52 +08:00
97851c2595 修正代理主钱包余额快照口径
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m0s
代理主钱包扣款流水必须记录扣款前后的真实余额,避免资金概况与流水展示出现双重扣减错觉。

Constraint: 项目禁止自动化测试,使用构建和数据库对账验证。

Rejected: 在资金汇总接口按历史充值总量或流水末条余额展示 | 资金概况口径应来自主钱包剩余余额。

Confidence: high

Scope-risk: narrow

Directive: 修改代理钱包扣款链路时必须同时维护余额字段与流水快照一致性。

Tested: go build ./...;migrate up 已执行到 141;只读 SQL 对账确认主钱包余额、成功流水累计、最新流水余额一致且 mismatch_tx_count=0。

Not-tested: 未新增自动化测试(项目规范禁止)。
2026-05-11 10:22:48 +08:00
ebaf112c80 新增排查自己的逻辑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-05-09 15:22:29 +08:00
09cf0be86e 联级,多选
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m52s
2026-05-09 15:06:27 +08:00
6a6672d0e4 允许代理看
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m11s
2026-05-09 11:03:32 +08:00
0b86720534 chore: add placeholder file
Co-authored-by: aider (openai/gpt-5.5) <aider@aider.chat>
2026-05-09 10:35:22 +08:00
a93acc0784 有效天数问题,充值单回调问题 2026-05-08 11:46:34 +08:00
7c85a8cf29 修复支付以及订单相关的问题
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1s
2026-05-08 10:15:03 +08:00
348cb4e670 修复绑定的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
2026-05-07 18:18:11 +08:00
e77bb0d09b 跨域
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m57s
2026-05-07 17:16:54 +08:00
73a3a04204 跨域
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m59s
2026-05-07 16:28:23 +08:00
573e887ca5 灰度脚本 2026-05-07 11:28:42 +08:00
29d3d48dbe 黑魔法
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
2026-05-06 16:59:10 +08:00
d42fe2b0ca 补上参数
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-05-06 16:44:14 +08:00
eb74dd7849 补回是否启用虚流量
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m58s
2026-05-06 16:07:59 +08:00
3a2e3f2571 浅浅升级一下
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m8s
2026-05-06 14:41:14 +08:00
b0bd37ec12 尝试优化一下
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m0s
2026-05-06 10:21:38 +08:00
001f8caf35 修复当前卡不对的问题 2026-05-06 10:19:21 +08:00
21b73a750d c端当前套餐开始时间过期时间,以及excel导入字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
2026-05-06 09:57:58 +08:00
ee7bfb81f0 修复解绑卡没有iccid的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m56s
2026-05-06 09:44:15 +08:00
b4a9e0c1c9 归档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m52s
2026-04-30 15:59:40 +08:00
948243b13d 修改 2026-04-30 15:49:40 +08:00
5425e2ce19 保留提案基线以便团队 worktree 按契约执行
Constraint: omx team 默认使用独立 worktree,要求 leader 工作区干净
Rejected: git stash 提案文件 | 会让 worker 无法读取当前提案契约
Confidence: high
Scope-risk: narrow
Directive: 后续实现必须继续严格按 tasks.md 顺序推进,不得擅自跳步
Tested: git status 仅剩未跟踪的 .omx 上下文文件
Not-tested: 未验证提案内容本身的实现正确性
2026-04-30 15:11:32 +08:00
70fb7dadef 归档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-04-30 11:58:33 +08:00
8a3c45c577 设备导入卡槽不对的问题 2026-04-30 11:56:09 +08:00
9238eeb56e 更新提案 2026-04-30 11:18:13 +08:00
9674e0d0d9 文档更新 2026-04-30 10:54:29 +08:00
248ed55f4e 获取当前生效的appid
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m46s
2026-04-30 10:52:33 +08:00
66e12e9629 订单相关以及佣金相关修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
2026-04-30 09:52:39 +08:00
4f4e42f27e 应当返回真流量
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m1s
2026-04-29 16:19:15 +08:00
6b3b33b2f0 废除激活时间
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
2026-04-29 12:04:23 +08:00
74c7b27327 切卡模式更新
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m58s
2026-04-29 10:18:15 +08:00
37b4483183 赠送套餐逻辑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
2026-04-28 17:40:06 +08:00
516a7f5bdf 优化后台创建订单的代理套餐下架提示
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m59s
2026-04-28 14:27:29 +08:00
fe4c545308 修正数据
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
2026-04-27 18:10:08 +08:00
bb33232b1b 日志记录不全
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
2026-04-27 15:32:17 +08:00
c3ae7fcfbc 修复设备WiFi上游请求参数
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m40s
2026-04-27 15:19:23 +08:00
66cec7515a 更新
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m18s
2026-04-27 14:52:17 +08:00
cb75b5668b 操作日志
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m10s
2026-04-27 12:16:38 +08:00
95104100c9 修复企业授权报错的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
2026-04-27 10:30:10 +08:00
02ccf57aa3 平台应当要可以用代理的钱包支付
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m6s
2026-04-27 10:01:38 +08:00
60debb7505 统一导出任务
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m4s
2026-04-27 09:47:03 +08:00
531de3c760 已退回后的还是可以继续提交
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m49s
2026-04-27 09:29:54 +08:00
a887f91686 修复卡轮训的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m14s
2026-04-26 12:41:16 +08:00
7efcea2b54 修复错误报错
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m0s
2026-04-24 17:53:22 +08:00
85bef83752 跨级
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
2026-04-24 16:33:13 +08:00
bc344f892c 新增日志输出
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
2026-04-24 11:25:13 +08:00
41db5b56af 上级店铺名称
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-04-24 11:20:41 +08:00
15cd26c81a 强制手机绑定
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m1s
2026-04-24 10:26:32 +08:00
3f2cf31543 校验修复以及查询修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
2026-04-24 09:41:51 +08:00
ba73913a41 当前卡的逻辑变更
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m53s
2026-04-24 09:25:44 +08:00
a44c88005d 手动实名
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m24s
2026-04-23 17:35:38 +08:00
f177c26016 快照订单号以及退款订单号
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s
2026-04-23 17:14:10 +08:00
4bde09837a 激活时间
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m35s
2026-04-23 17:02:11 +08:00
c250a47651 卡不激活的问题 2026-04-23 16:58:16 +08:00
a7f2c4480a 同步更新订单状态,校验退款金额小于等于订单实付金额
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m16s
2026-04-23 15:40:59 +08:00
37e113bbf4 新增查询字段
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-04-23 15:33:15 +08:00
5442dcbeda 时间支持
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m4s
2026-04-23 14:20:02 +08:00
b9fc828567 订单总价和订单明细单价在代购/成本价场景会不一致,已改为同一套价格来源并支持多套餐求和
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m24s
2026-04-23 12:11:48 +08:00
327c72ca74 修复查询问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s
2026-04-23 11:48:59 +08:00
32f03cd2c1 新增退款时应当快照当时的资产
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-04-23 11:43:15 +08:00
8d2e8faa84 修复退款bug
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m43s
2026-04-23 11:30:46 +08:00
1f1f31a7cb 退款列表快照订单号
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m30s
2026-04-23 10:59:34 +08:00
52d883d8a0 修复店铺名不存在的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m29s
2026-04-23 10:21:23 +08:00
6b1fbf4a92 修复金额的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m30s
2026-04-23 10:09:23 +08:00
10a35625a5 新增状态筛选条件,同时修复价格未正常写入的情况
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m35s
2026-04-23 09:57:39 +08:00
a207c51bfb 修复设备套餐流量不同步的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m32s
2026-04-22 18:04:53 +08:00
6eb6d381f7 日志
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m32s
2026-04-22 16:58:04 +08:00
e27758cce3 修复新卡导入时没有正确填写停机原因的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m45s
2026-04-22 16:41:33 +08:00
7137ed087f 修复序列化的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m29s
2026-04-22 15:24:39 +08:00
a9690a49db 提交
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
2026-04-22 15:06:01 +08:00
0f58886454 增加日志,提交流程图
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m42s
2026-04-22 14:49:09 +08:00
9f8173e124 灰度上线流程图准备 2026-04-21 16:42:47 +08:00
1d930cc82d 运营商名称问题TODO
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m29s
2026-04-21 11:27:19 +08:00
1b21d4dafa 新增运营商名称模糊搜索 2026-04-21 11:24:47 +08:00
e573a82120 设备卡列表没有正确返回网络状态以及实名认证策略的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m34s
2026-04-21 11:11:03 +08:00
e049080f6c 分裂iccid长度
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m47s
2026-04-21 10:55:12 +08:00
9942bbc74e docs: 更新实名认证策略 DTO description 默认值说明
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m35s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-20 15:39:47 +08:00
fc9f819787 feat: 修改实名认证策略默认值为先充值后实名(after_order)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-20 15:39:01 +08:00
33a4d25ca9 chore: 新增数据库迁移,将实名认证策略默认值改为 after_order
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-20 15:29:25 +08:00
e9862a67ba fix: 修复资产卡列表未返回 realname_policy 字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m10s
buildDeviceResolveResponse 和 GetRealtimeStatus 中构建 BoundCardInfo 时
均遗漏了 RealnamePolicy 字段赋值,导致 resolve 和 realtime-status
两个接口的设备卡列表不返回实名认证策略。字段在 Model 和 DTO 中均
已定义,Service 层补充赋值即可。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-20 14:43:09 +08:00
868f5a8ef3 fix: 修复轮询停机连坐、实名逆转防抖及carrier_stopped原因写入缺失三个逻辑缺陷
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
2026-04-20 11:24:56 +08:00
fd795d8742 feat: 新增设备切卡模式接口 SetSwitchMode
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m32s
2026-04-18 16:22:30 +08:00
2b3a9cb33f feat: 新增套餐使用记录实付金额快照字段 paid_amount
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m19s
- 新增迁移 000129:tb_package_usage 添加 paid_amount BIGINT 字段,存量数据通过 JOIN tb_order 回填
- PackageUsage Model 新增 PaidAmount *int64 字段
- 4 个写入点(order service 主套餐/加油包、auto_purchase 主套餐/加油包)赋值 order.ActualPaidAmount
- AssetPackageResponse DTO 新增 paid_amount 字段
- GetCurrentPackage / GetPackages 填充 paid_amount,接口直接返回购买价格无需 JOIN 订单表
2026-04-18 10:19:21 +08:00
4b8784d5e7 docs: 更新 OpenAPI 文档,新增批量下载 URL 接口文档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m15s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-18 10:08:53 +08:00
2413c2fe14 feat: 新增批量获取文件下载预签名 URL 接口
新增 POST /api/admin/storage/batch-download-urls,支持一次传入最多 50 个 file_key,返回对应预签名下载 URL 映射。解决列表页批量展示图片需多次请求的问题。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-18 10:08:45 +08:00
e52671eaed 归档提案 2026-04-18 09:35:27 +08:00
716e9f5971 chore: 更新 fix-realname-activation-logic OpenSpec 任务清单
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m15s
标记全部 13 个任务为已完成

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-18 09:30:56 +08:00
28ac58ea18 feat: PollingRealnameHandler 新增设备级套餐实名激活联动
- 结构体新增 deviceSimBindingStore 字段,构造函数参数列表末尾追加
- 新增 triggerDeviceRealnameActivation:卡首次实名(0→1)时查询所属设备,
  若有绑定则提交 carrier_type=device 的 TaskTypePackageFirstActivation 任务
- 独立卡(无绑定设备)静默跳过,任务提交失败仅记录 Warn 日志不阻断流程
- registerPollingHandlers 传入 h.workerResult.Stores.DeviceSimBinding
- 修复「购买后某张卡实名」场景下设备级套餐永不激活的缺陷

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-18 09:30:50 +08:00
8e61cb1a54 fix: 购买时检查载体实名状态,已实名跳过 pending 直接激活
- iot_card 载体:扩展查询字段同时读取 real_name_status,已实名则保持 Active 不切换 pending
- device 载体:通过 GORM 子查询统计绑定卡中已实名数量,count>0 则直接激活
- 查询出错时保守默认 false,不阻断购买流程
- 修复「先实名后购买」场景下套餐永久 pending 及购买后不自动复机的双重缺陷

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-18 09:30:40 +08:00
4aab0bcbf2 提案以及归档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m39s
2026-04-18 09:10:29 +08:00
6caf0f6141 feat: 新增全局操作密码功能,将线下充值验证从登录密码改为统一操作密码
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增 RedisSystemOperationPasswordKey 常量函数(pkg/constants/redis.go)
- 新增 operation_password Service(Set/IsSet/Verify,bcrypt 哈希存 Redis)
- 新增 SuperAdminHandler 及两个接口:
  - POST /api/admin/super-admin/operation-password(设置/重置,仅超级管理员)
  - GET /api/admin/super-admin/operation-password/status(查询是否已设置)
- AgentRecharge.OfflinePay 操作密码验证从"查当前用户登录密码"改为"全局操作密码 Verify"
- Bootstrap/路由/文档生成器同步注册
2026-04-18 09:06:33 +08:00
6e15e1b853 docs: 更新 OpenAPI 文档及提案任务清单,新增 payment_voucher_key 字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m26s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 18:29:42 +08:00
ad30f5d41b feat: 代理充值支持线下支付凭证上传与存储
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 18:29:06 +08:00
b538e45bd9 feat: 新增数据库迁移,为 tb_agent_recharge_record 添加 payment_voucher_key 列
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 18:28:38 +08:00
1033d7666b 修复主账号问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m6s
2026-04-17 18:07:29 +08:00
0d2baabcf1 chore: 更新 asset-realname-policy 提案任务清单,勾选全部已完成项
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m24s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:33:52 +08:00
9d8e3926ff docs: 更新 OpenAPI 文档,新增 realname-mode 更新接口文档
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:33:07 +08:00
c5ed040359 feat: 后台管理新增 UpdateRealnameMode 接口并注册路由 PATCH /api/admin/assets/:identifier/realname-mode
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:32:18 +08:00
363e0efb34 feat: C 端充值、购买、实名链接接口接入实名策略拦截(before_order/after_order)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:31:21 +08:00
1363797378 feat: 导入 Service 和 Worker 传递 realname_policy 写入资产记录
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:31:01 +08:00
71498883b5 feat: iot_card/device Service 新增 UpdateRealnamePolicy 方法(含幂等检查)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:30:13 +08:00
1d75a4c31a feat: asset Service 新增 GetEffectiveRealnamePolicy 策略判断及 HasValidRechargeOrPaidOrder 检查逻辑
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:29:32 +08:00
89eec1406a feat: iot_card/device Store 新增 UpdateRealnamePolicy 方法
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:28:58 +08:00
9a625f51ef feat: C 端查询及设备导入接口 DTO 新增 realname_policy 字段
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:28:20 +08:00
2d6456c4a3 feat: 后台管理查询接口 DTO 新增 realname_policy 字段
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:27:10 +08:00
9bd1d60e85 feat: 四个 Model 新增 realname_policy 字段(含 gorm 标签和中文注释)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:26:20 +08:00
908c5fa1de feat: 新增 RealnamePolicy 常量枚举及 CodeRealnameNotAvailable 错误码
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 17:25:02 +08:00
505b30aa8f feat: 实现资产实名认证策略功能 (asset-realname-policy)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m44s
- 新增 realname_policy 字段到 tb_iot_card、tb_device、tb_iot_card_import_task、tb_device_import_task 四张表
- 策略枚举:none(无需实名)、before_order(先实名后充值/购买)、after_order(先充值/购买后实名)
- C端充值拦截:before_order 策略下未实名用户禁止充值
- C端订单拦截:before_order 策略下未实名用户禁止购买套餐
- C端实名链接拦截:after_order 策略下无充值/订单用户禁止实名认证
- Admin API:PATCH /api/admin/assets/:identifier/realname-mode
- 新增错误码 CodeRealnameNotAvailable=1189
2026-04-17 17:03:41 +08:00
e0a37f4a11 fix: verify-asset 接口增加卡绑定设备校验
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m29s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 14:49:11 +08:00
4c284c422c fix: 移除 IoT 卡导入 ICCID 长度限制
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m29s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 12:04:44 +08:00
44e4f03957 docs: 补充微信 JSSDK 配置接口 OpenAPI 文档
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-17 12:04:35 +08:00
0a2961ecd6 fix: 支付回调补写 actual_paid_amount 字段,从支付平台回调中提取实付金额
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
- HandlePaymentCallback 新增 actualPaidAmount int64 参数,Updates 写入 actual_paid_amount
- 微信 V2 回调从 TotalFee 字符串解析分值;V3 直接使用 TotalAmount
- 支付宝回调优先取 buyer_pay_amount(实付),回退 total_amount,元转分使用 math.Round
- 富友回调从 OrderAmt 字符串解析分值
- dispatchWechatCallback 同步增加 paidAmount 参数透传
2026-04-17 11:12:35 +08:00
fa554c3930 feat: 注册 C 端微信 JSSDK 配置接口路由及文档生成
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m35s
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-16 18:38:43 +08:00
1125701329 feat: 新增 C 端微信 JSSDK 签名配置接口 Handler
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-16 18:38:20 +08:00
ed1facc1b8 feat: 公众号服务新增 GetJSSDKConfig 方法及 JSSDK 配置 DTO
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-16 18:37:59 +08:00
0cba6fd6d5 fix: 修复轮询缓存竞态导致流量增量重复计全量的问题
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
getCardWithCache 在缓存 miss 时将 writeCardToCache 由异步协程改为同步调用。
原实现中协程可能在 updateCardCache 之后才被调度执行,将
last_gateway_reading_mb 覆盖回 DB 旧值(0),导致下次轮询
increment = gatewayFlowMB - 0 = 全量,触发套餐流量重复扣减。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-16 18:33:46 +08:00
94f20ce8c7 配置
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m12s
2026-04-16 17:50:49 +08:00
aef20d2ab1 fix: 修复禁用所有轮询配置重启后再启用无法工作的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m23s
- 新增 RedisPollingConfigChangedChannel 常量,用于跨进程配置变更通知
- ConfigService 注入 Redis,Create/Delete/UpdateStatus 后发布 Pub/Sub 事件
- PollingConfigManager 新增 WatchChanges() 方法,收到通知立即刷新内存缓存
- PollingInitializer 新增 Restart() 方法,支持初始化完成后安全重启(CAS 防并发)
- Worker 订阅配置变更事件,configs 从空变为非空时触发 Initializer.Restart()

根因:启动时所有配置禁用导致分片队列为空,Initializer 是一次性的,
之后启用配置只更新 DB 但不重建队列,调度器无卡可处理
2026-04-16 17:32:43 +08:00
0ec16d4afa fix: 实名轮询防止上游接口故障时误降级已实名状态
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m34s
Gateway 返回 data=null 时 sonic 解析为零值结构体(ICCID="" RealStatus=false),
导致已实名卡被误判为未实名并触发停机。

通过检查响应中 ICCID 是否回填来判断数据有效性:
- ICCID 为空:视为上游接口故障,重新入队,不更新实名状态
- ICCID 有值:响应有效,正常处理(含合法的未实名降级)
2026-04-16 16:48:14 +08:00
7e4c61e6e9 docs: 新增枚举状态字段规范及 Code Review 检查清单
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-16 15:58:28 +08:00
2d0b4e9bc0 fix: 充值订单列表按当前登录资产过滤,修复越权查看其他资产数据的漏洞
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-16 15:56:43 +08:00
520b126ecf feat: JWT Claims 新增资产字段,登录 token 携带当前资产上下文
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-16 15:55:41 +08:00
5d9be1d7e4 fix: 修正轮询配置多匹配逻辑,支持同卡匹配多个配置并按 priority 合并 interval
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s
核心变更:
- MatchConfig 改为 MatchConfigs,返回所有匹配配置
- MergedTaskIntervals 按 task type 合并各配置,选取最高优先级(非 nil 且最小 priority 值)
- hasAnyEnabledInterval 过滤所有 interval 均为 NULL 的配置
- calcInitialDelay 重构为纯函数,接收 interval 参数
- 移除 getEnabledTaskTypes 和 getIntervalByTaskType(被 MergedTaskIntervals 替代)
- scheduler.go 新增心跳 key + 顶层 panic recovery + Init 完成守卫
- initializer.go 批量失败日志升级为 Error,逐条检查 Pipeline 命令错误
- 数据迁移:禁用 id=29 的轮询配置(所有 interval 均为 NULL)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 14:27:47 +08:00
5065d925ad fix: 修正套餐激活和时间字段nullable问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
核心变更:
1. Model层时间字段改为*time.Time并设为nullable
   - PackageUsage.ActivatedAt/ExpiresAt
   - PersonalCustomerDevice/ICCID/Phone.LastUsedAt/VerifiedAt

2. 数据库迁移:
   - activated_at/expires_at列移除NOT NULL约束
   - 清洗零值记录(status=0且activated_at<'2000-01-01')

3. 新增ActivateSpecificPackage方法:精准激活指定套餐,
   修复HandlePackageQueueActivation从"查找过期包"改为直接激活payload指定套餐

4. 新增孤儿套餐恢复扫描:Worker启动或每次套餐检查时,
   自动发现并恢复无status=1主套餐的孤儿载体

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 11:14:39 +08:00
97d6319b64 fix: 修正 HasValidByCarrier 有效套餐定义,仅 status=Active 才算有效
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m43s
修复Bug:原代码将 Pending(0) 也算作有效套餐,导致待生效套餐的卡
不会被触发停机指令。现改为仅生效中(Active)才算有效套餐。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 18:14:50 +08:00
0a27a78d97 fix: 修正强充订单状态映射错误,0-based DB 常量正确映射为 1-based 客户端状态
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 48s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 14:58:37 +08:00
fc995187df 输出日志
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 49s
2026-04-15 14:11:42 +08:00
8bf1282378 归档: add-polling-card-status-with-verbose-log,同步主规范
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m24s
change 目录归档至 openspec/changes/archive/2026-04-15-add-polling-card-status-with-verbose-log/
新建主规范 openspec/specs/polling-card-status-task/spec.md(6个需求)
追加 verbose log 需求至 openspec/specs/polling-task-handlers/spec.md(4个新需求)
protect handler 规范更新字段名:network_status → network_status_at_check + action_taken

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:23:05 +08:00
854330a4b4 docs: 更新 OpenAPI 文档,新增卡状态检查间隔字段
PollingConfig 请求/响应结构中添加 card_status_check_interval 字段说明

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:22:55 +08:00
e40ad462e1 feat: 注册卡状态轮询 Worker Handler
registerPollingHandlers 创建并注册 PollingCardStatusHandler
日志更新为 realname/carddata/package/protect/card_status

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:22:49 +08:00
647d78309d feat: 将卡状态任务类型接入队列管理、生命周期和初始化器
queue_manager.go: allTaskTypes 追加 TaskTypePollingCardStatus,注释更正为5个队列
lifecycle_service.go: getEnabledTaskTypes 和 calcInitialDelay 新增 card_status 条件
initializer.go: initBatch 新增 CardStatusCheckInterval 块,以 LastCardStatusCheckAt 为基准写入分片队列

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:22:42 +08:00
c7f486ae09 feat: 新增 PollingCardStatusHandler 及工具函数卡状态支持
polling_cardstatus_handler.go: 新文件,实现卡开停机状态轮询
  - Gateway QueryCardStatus → 停机/0,其他/1
  - 状态变化时写 DB + 更新缓存 + 触发 EvaluateAndAct
  - verbose log 位于 gateway 块内
polling_utils.go: getIntervalByTaskType 新增 TaskTypePollingCardStatus case

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:22:34 +08:00
71559547b6 feat: 为四种现有轮询 Handler 添加 verbose log 输出
realname: verbose log 移入 gateway 块内,避免 gateway=nil 时输出零值
carddata: 添加 && h.gateway != nil 门卫,确保仅 gateway 查询成功时输出
package: verbose log 在 freshCard 加载后、EvaluateAndAct 之前输出
protect: 引入 actionTaken 变量,字段名 network_status → network_status_at_check,新增 action_taken

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:22:25 +08:00
6292443f69 feat: PollingBase 新增 verboseLog 支持,Worker 传入配置
polling_base.go: PollingBase.verboseLog bool 字段,NewPollingBase 新增参数
main.go: cfg.Polling.VerboseLog 作为第6个参数传入 NewPollingBase

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:22:16 +08:00
97c196b7c6 feat: PollingConfig DTO 和 Service 支持卡状态检查间隔
dto: 新增 CardStatusCheckInterval 字段(validate min=30,description 含枚举说明)
config_service.go: 创建/更新/响应映射均支持 CardStatusCheckInterval

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:22:09 +08:00
cb328f3ec2 feat: PollingConfig 和 IotCard 模型新增卡状态检查字段
polling.go: CardStatusCheckInterval *int(card_status_check_interval)
iot_card.go: LastCardStatusCheckAt *time.Time(last_card_status_check_at)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:22:01 +08:00
9992e82a3c feat: 新增卡状态轮询任务类型常量及 verbose_log 配置
constants.go: 新增 TaskTypePollingCardStatus = "polling:card_status"
config.go: 新增 PollingConfig struct 含 VerboseLog bool
config.yaml: polling.verbose_log 默认 false

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:21:43 +08:00
e2003eb9e3 feat: 新增卡状态检查轮询数据库迁移
tb_polling_config 新增 card_status_check_interval INT NULL
tb_iot_card 新增 last_card_status_check_at TIMESTAMPTZ NULL
含 IF NOT EXISTS 保证幂等,含完整回滚文件

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 12:21:21 +08:00
71048e31d4 更换新的上游域名
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m15s
2026-04-15 11:21:14 +08:00
4a1e44f9e1 提案
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-04-15 11:17:23 +08:00
dd408fcdca 日志输出 2026-04-15 11:17:09 +08:00
516bb92a16 refactor: 删除 enterprise_device service 中无路由接入的死代码方法
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
ListDevicesForEnterprise、GetDeviceDetail、SuspendCard、ResumeCard、validateCardOperation 均无对应路由和 Handler 调用,且 SuspendCard/ResumeCard 仅修改本地 DB 未调 Gateway,存在错误实现风险。统一使用 iot_card stop_resume_service 处理停复机。

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 11:16:36 +08:00
023a4309eb fix: 修正 realtime-status 路由文档描述,补充说明设备类型会实时调 Gateway
原描述误写为不调网关,实际 GetRealtimeStatus 对设备类型会调用 SyncDeviceInfo 接口。

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-15 11:16:01 +08:00
b972a776d9 重构充值订单模块:用 tb_recharge_order + tb_payment 替换 tb_asset_recharge_record
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m32s
- 新增 RechargeOrder 和 Payment 模型及对应 Store
- 新增 ClientRechargeOrderHandler 提供充值订单列表/详情接口
- 修改 client_wallet.go 使用新表读写充值数据
- 修改 callback/payment.go 将 CRCH 订单路由到 rechargeOrderService
- 修改 client_order/service.go 的强充流程使用新表
- 修改 auto_purchase.go 从 tb_recharge_order 读取 linked_package_ids
- 修改 order/service.go 的 WalletPay 使用 tb_payment 记录
- 修改 wechat_config_store.go 从 tb_recharge_order 统计待支付充值数
- 移除 AssetRechargeStore 和 AssetRechargeRecord 的注册引用
- 修复文档生成器缺失 ClientRechargeOrder handler
- 状态枚举改为 0-based: Pending=0, Paid=1, Closed=2, Refunded=3
2026-04-15 11:00:32 +08:00
d26010b29d 重构用的提案,现在不敢跑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 49s
2026-04-14 17:38:36 +08:00
8706247436 fix: 资产解析接口 BoundCardInfo 添加运营商字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m18s
2026-04-14 17:10:28 +08:00
8154a61453 fix: 移除 standaloneListColumns 中已废弃的分佣字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m51s
迁移 000116 已从 tb_iot_card 删除 first_commission_paid 和 accumulated_recharge
两列,但 standaloneListColumns 未同步清理,导致 SELECT 查询报列不存在错误。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 12:40:04 +08:00
d9de704d73 归档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 49s
2026-04-14 12:14:29 +08:00
4ea4a1e53f docs: 更新 tech-debt-cleanup tasks.md 实际完成状态 2026-04-14 11:59:28 +08:00
041856dc8c docs: 新增迁移说明文档,reset_db.sh 添加 000114 基线检查
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s
2026-04-14 11:56:50 +08:00
7cdb66cbe4 refactor: 统一状态名称映射为公共函数,清理废弃换卡/商户类型常量和模型 2026-04-14 11:51:58 +08:00
5410181e77 修复:C端资产信息接口 lan_ip 字段无值时不返回的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m17s
去掉 DeviceRealtimeInfo.LANIP 的 omitempty 标签,
确保 device_realtime.lan_ip 始终出现在响应中(无值时返回 null)
2026-04-14 11:49:17 +08:00
548ec0cd6b feat: 补全 _name 字段公共映射函数,修正实名认证注释 2026-04-14 11:44:23 +08:00
5e9e41db21 feat: 归档旧迁移文件,创建初始化数据迁移和重置脚本 2026-04-14 11:24:35 +08:00
c35542f911 feat: 补全客户端订单和资产 DTO _name 字段,修复代码格式 2026-04-14 11:19:57 +08:00
42c5ec912f feat: 技术债务清理(支付配置动态化、API文档补全、轮询常量提取、废弃代码清理)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s
2026-04-14 11:11:15 +08:00
c0b64c9e30 fix: 登录时将主手机号写入 JWT,修复 bound_phone 返回空的问题
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-04-14 11:09:42 +08:00
37706bc7ce feat: 资产信息接口返回当前登录用户绑定手机号
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m30s
2026-04-14 10:08:42 +08:00
7308afe801 归档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m18s
2026-04-13 15:03:02 +08:00
c1456f3c54 docs: 补充轮询自动触发环境变量说明 2026-04-13 14:57:00 +08:00
640ea8ec99 feat: 更新依赖注入,传入 ManualTriggerService 2026-04-13 14:52:50 +08:00
8569873690 feat: ClientRealnameHandler 集成异步触发实名检查 2026-04-13 14:51:42 +08:00
5196472d7e fix: 修复手动触发去重TTL和日限制次数,优化权限检查和错误日志 2026-04-13 14:50:14 +08:00
814a2956b3 feat: 新增轮询自动触发配置(EnableAutoTrigger、AutoTriggerSystemUserID) 2026-04-13 14:48:12 +08:00
4879d9bf07 fix: 修复公众号登录初始化失败(PowerWeChat 日志写入权限)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m29s
NewOfficialAccountAppFromConfig 未设置 Log.Stdout=true,PowerWeChat SDK
默认尝试在工作目录创建 wechat/info.log 文件,容器环境无写权限导致
NewOfficialAccount 返回 permission denied,触发 1181 配置不可用错误。

支付配置已有此修复(NewPaymentAppFromConfig),公众号配置漏掉了。

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-13 14:39:54 +08:00
8d8d426e2e feat: verify-asset 接口返回当前生效的公众号和小程序 AppID
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m51s
2026-04-11 17:22:21 +08:00
246ae5e123 docs: 新增 status-convention 主规范
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m20s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 12:21:07 +08:00
7e613a0d2b 归档: fix-status-convention-comments
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 12:21:02 +08:00
510a5bfb21 chore: 删除未使用的僵尸状态常量 DevCapabilityStatusEnabled/Disabled
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 12:20:56 +08:00
decb7a10ec fix: 使用专用常量 ShelfStatusOn 替代 StatusEnabled 赋值 ShelfStatus
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 12:20:49 +08:00
5588c7c762 fix: 修正 DTO status 字段 description 和 validate 标签
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 12:20:43 +08:00
eebc7194d8 fix: 修正 model status 字段 GORM comment 为 0=禁用 1=启用
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 12:20:31 +08:00
e945a672ed fix: 修正套餐系列状态注释格式
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m19s
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 12:00:01 +08:00
41d3667df2 feat: 设备卡列表新增运营商类型字段
在 DeviceCardItem DTO 和 GetDeviceCards Handler 中补充 carrier_type 字段,解决设备视角卡列表缺少运营商类型的问题

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 11:59:54 +08:00
8f66ca7bcb 文档提交
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m14s
2026-04-11 11:27:43 +08:00
e9ff14df0e 规范: 新增枚举与状态字段规范并修复代理充值状态描述错误
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增 docs/enum-status-standards.md:完整的枚举/状态规范,涵盖 int vs string 选择、起始值约定、description 格式统一、constants 唯一真相来源、Response DTO 必须提供 status_name
- 更新 dto-standards SKILL.md:加入枚举规范要点,AI 写 DTO 时自动触发
- 更新 AGENTS.md:常量管理章节和 Code Review 检查清单各加入枚举检查项
- 修复 agent_recharge_dto.go:description 从错误的 3 个状态改为正确的 5 个状态(原 3:已取消 实为 3:已完成)
- 修复 agent_recharge service:toResponse 补充 StatusName 中文字段,防止前端映射出错
2026-04-11 11:25:58 +08:00
c807b99a91 fix: 修复佣金明细列表多表 JOIN 时 shop_id 字段歧义问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m24s
ListByShopID 使用 tb_commission_record.shop_id 限定表名,
避免与 JOIN 的 tb_order/tb_iot_card/tb_device 表中同名字段冲突,
解决 SQLSTATE 42702 ambiguous column reference 错误。

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 11:06:13 +08:00
677d6239ce fix: 统一佣金状态常量并修复佣金明细关联查询
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
- 删除 model/commission.go 中与 constants 包冲突的旧常量(值=1/2)
- 所有服务层改用 constants.CommissionStatusReleased(值=3)写入和查询
- 数据库迁移:status 1→3(已发放),2→4(已失效)
- 修复佣金明细列表接口,通过 JOIN 关联返回 order_no、iccid、virtual_no、order_created_at
- 新增 seller_shop_id / seller_shop_name 销售来源字段
- 统计接口过滤条件从精确匹配改为排除无效(NOT IN 4,99)
- 更新 OpenAPI 文档及 commission-record-query spec
2026-04-11 10:42:37 +08:00
d0989c66bb 暂且不需要实名
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m19s
2026-04-10 18:31:40 +08:00
67f3286e09 fix: 修复店铺资金摘要接口username和phone字段为空的bug
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m17s
问题原因:GetPrimaryAccountsByShopIDs 在 Store 层重复应用了数据权限过滤,
导致当前用户无权限访问的店铺账号被错误过滤,accountMap 为 nil,
最终返回的 username 和 phone 为空字符串。

解决方案:移除 Store 层的 ApplyShopFilter 权限过滤,
因为调用方(Service 层)已经保证了 shopIDs 的合法性。
2026-04-10 17:21:27 +08:00
a8b52f8ae1 fix: 设备详情 RealNameStatus 取当前使用卡而非聚合最小值
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-04-10 17:15:40 +08:00
5496cb58aa feat: 订单创建时快照买家手机号/昵称和套餐类型
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
新增字段:
- tb_order: buyer_phone, buyer_nickname(个人客户下单时快照)
- tb_order_item: package_type(套餐类型快照)

后台订单列表支持按 buyer_phone 精确过滤查询。

OpenSpec: order-buyer-snapshot
2026-04-10 17:12:56 +08:00
3cb16804a4 fix: 设备资产详情返回 enable_polling 字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m19s
2026-04-10 15:54:47 +08:00
2a7ac3f86e fix: 修复轮询系统缓存不一致和可观测性问题
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- OnCardStatusChanged/Enabled/Disabled 添加 InvalidateCardCache,
  解决 Refresh API 更新 DB 后 polling 缓存仍为旧值的 bug
- AssetResolveResponse 的 enable_polling/network_status 去掉
  omitempty,解决 false/0 时字段从响应中消失的问题
- Scheduler/Initializer/PackageHandler 增加 INFO 级别日志,
  可通过日志判断轮询是否工作、处理了哪些卡
2026-04-10 15:52:38 +08:00
ef8ec025bd feat: 资产套餐接口返回是否开启虚流量字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m26s
current-package 和 packages 两个接口的响应中新增
enable_virtual_data 字段,表示该套餐是否启用虚流量

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:33:05 +08:00
23e12d8a73 fix: 卡导入 Excel 解析移除虚拟号强制校验
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m19s
虚拟号(virtual_no)为可选字段,解析时不应强制要求填写。
与 processBatch 中的处理逻辑保持一致。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:58:39 +08:00
4a18aac6ce fix: 资产验证支持虚拟号(virtual_no)查询
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s
resolveAsset 查询 IoT 卡时仅匹配 iccid,导致用户输入
虚拟号时返回"资产不存在"(1180)。将查询条件扩展为
iccid OR virtual_no,与设备查询逻辑保持一致。

顺带将 jwt.ParseWithClaims 回调的 interface{} 替换为 any。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:42:23 +08:00
51fa027eb8 归档 2026-04-09 17:47:06 +08:00
afc6d7fc76 docs: 归档 excel-import-refactor,同步更新主 specs
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 50s
- 将变更目录移至 archive/2026-04-09-excel-import-refactor
- device-import spec:更新为固定列位置读取(IMEI 调整至第5列),新增 MaxSimSlots 范围校验说明
- iot-card-import-task spec:更新为固定列位置读取,新增"导入批次支持卡业务类型"需求

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:37:44 +08:00
cd807e58e3 feat: Excel 导入按列位置读取,IoT 卡导入支持卡业务类型
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s
- 重写 parseCardRows:移除列名识别,固定跳过第1行表头,按 col0=ICCID、
  col1=MSISDN、col2=VirtualNo 取值,VirtualNo 升级为必填字段
- 重写 ParseDeviceExcel:移除 buildDeviceColumnIndex,IMEI 调整至第5列
  新增 MaxSimSlots 范围校验 [1,4],整行为空时不计入 total
- 删除 findCardColumns 和 buildDeviceColumnIndex 两个冗余函数
- ImportIotCardRequest 新增 card_category 字段(normal/industry,默认 normal)
- IotCardImportTask model 新增 card_category 字段
- 迁移 000111:tb_iot_card_import_task 新增 card_category 列
- CreateImportTask 将 req.CardCategory 写入任务,空串兜底 normal
- processBatch 改用 task.CardCategory 替代写死的 CardCategoryNormal
- ImportTaskResponse 新增 card_category 字段返回

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:16:31 +08:00
ff7e749bf2 归档 2026-04-09 14:53:48 +08:00
0627ffec42 feat: 代理商资金可见性重构(agent-fund-visibility)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m10s
- 将 GET /shops/commission-summary 重命名为 GET /shops/fund-summary,
  响应新增 main_balance、main_frozen_balance 两个预充值钱包字段
- 新增 GET /shops/:id/main-wallet/transactions 预充值钱包流水接口
- 将佣金统计、每日统计、发起提现从 /my/ 路径迁移至 /shops/:id/ 路径:
  GET /shops/:id/commission-stats
  GET /shops/:id/commission-daily-stats
  POST /shops/:id/withdrawal-requests
- 删除 MyCommissionService、MyCommissionHandler 及全部 /my/ 路由
- 补齐 ListShopWithdrawalRequests、ListShopCommissionRecords 的
  CanManageShop 越权校验(安全修复)
- 提现接口增加严格权限:仅代理账号本人可为自己店铺发起提现

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:40:39 +08:00
1d6535b81b fix: 修复移除账号角色接口路由参数名错误
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
RemoveRole handler 中使用 c.Params("id") 取账号 ID,
但路由注册的参数名为 account_id,导致解析失败报"无效的账号 ID"。
修正为 c.Params("account_id")。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:38:01 +08:00
0f6fd42aff fix: 禁用角色时检查是否已分配给账号或店铺
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
角色禁用前新增分配情况检查,若角色已分配给账号或店铺则拒绝禁用操作,
需先移除相关分配后才能禁用,与删除角色的防护逻辑保持一致。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:35:55 +08:00
c1cec0aede fix: 修复C端订单差价佣金计算错误及自动购包订单系列ID缺失问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m11s
- C端普通订单(createPackageOrder)创建时未设置 SellerCostPrice,
  导致底层店铺得到整个零售价作为差价佣金,上级代理链无法获得任何佣金。
  修复:在 createPackageOrder 中查询卖家成本价并写入订单字段。

- purchase_validation 新增 GetCostPrice 方法,供 C 端订单查询代理渠道成本价。

- auto_purchase 自动购包订单未设置 SeriesID 和 SellerCostPrice,
  导致差价佣金计算被整体跳过。修复:在 ProcessTask 中从卡/设备获取
  SeriesID,从套餐分配记录获取 SellerCostPrice,传入 buildOrderAndItems。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:35:13 +08:00
6dfc5c0301 feat: 资产解析接口新增实名时间字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
- AssetResolveResponse 新增 real_name_at 字段:card 类型取 first_realname_at,device 类型取绑定卡中最小的实名时间
- BoundCardInfo 新增 real_name_at 字段:各卡自己的 first_realname_at
- RefreshCardDataFromGateway 手动刷新路径补写实名时间:检测到 0→1 变化时同步写入 first_realname_at
- 同步更新 asset-resolve 规格文档

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:12:28 +08:00
fce35e8a2d fix: 修复角色分配权限并发竞态导致唯一约束冲突报错
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m4s
- Store 层 Create 方法对唯一约束冲突幂等处理,消除并发写入报错
- Service 层预批量加载角色已有权限集合(1次查询),替换逐条 Exists 查询(N次查询),同时降低竞态窗口

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
2026-04-09 11:58:52 +08:00
81ba84cf05 feat: 新增店铺联级查询接口
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s
- 新增 GET /api/admin/shops/cascade 接口,支持按店铺名模糊查询和上级ID过滤
- Store 层新增 ListForCascade(支持数据权限过滤)和 GetParentIDsWithChildren 方法
- Service 层新增 ListCascade,批量判断 has_children 避免 N+1 查询
- 返回格式:[{id, shop_name, has_children}]

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
2026-04-09 11:43:37 +08:00
384a54164b feat: 新增批量移除角色权限接口
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增 DELETE /api/admin/roles/:role_id/permissions 接口
- Store 层新增 BatchDelete 方法,单次 SQL 批量软删除
- Service 层新增 BatchRemovePermissions 方法
- Handler 层新增 BatchRemovePermissions 处理函数
- DTO 新增 BatchRemovePermissionsRequest/Params
- 修复 openapi generator 对 DELETE 方法 requestBody 不生成的问题
- RouteSpec 新增 Body 字段,支持显式指定 JSON 请求体

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
2026-04-09 11:41:58 +08:00
a7dfe858c7 feat: 创建代理账号时自动分配店铺默认角色
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m22s
💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
2026-04-09 11:06:31 +08:00
e9df1e7ded feat: IoT卡新增绑定设备虚拟号快照字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s
- tb_iot_card 新增 device_virtual_no 字段,存储绑定设备的虚拟号快照
- 列表/详情接口响应新增 device_virtual_no 字段
- 列表接口 is_standalone 改为可选参数(不传返回全部卡)
- 移除列表接口 virtual_no 查询参数
- 绑卡/解绑时同步更新 device_virtual_no 快照
- 设备导入时批量写入 device_virtual_no 快照
- 归档 feat-iot-card-device-virtual-no 变更提案

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 10:50:46 +08:00
cc56e27c01 feat: 后台线下支付订单强制上传支付凭证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m15s
在后台创建订单接口中新增 payment_voucher_key 字段,线下支付方式
下该字段为必填,存储对象存储 file_key,便于后续追溯与审计。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:45:06 +08:00
78d6aded9b fix: 修复IoT卡导入时未写入carrier_type和carrier_name字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
processBatch 构建 IotCard 时只写了 CarrierID,遗漏了 CarrierType 和 CarrierName,
导致导入后所有卡的运营商名称和运营商类型为空,影响 /api/admin/iot-cards/standalone
和 /api/admin/assets/resolve/{identifier} 接口返回结果

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-08 11:56:57 +08:00
14a8ea5a2c fix: 单卡导入虚拟号改为可选,非空时保证全局唯一
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m26s
2026-04-08 10:45:23 +08:00
38ef73fd7f docs: 补充资产标识符标准化接口文档更新 2026-04-08 10:45:13 +08:00
303bffbe84 文档生成不正确的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m52s
2026-04-07 18:19:47 +08:00
80c6f6c756 feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
主要变更:
- 新增 AssetIdentifier 模型及 Store,统一管理资产标识符(ICCID/IMEI/SN 等)
- 新增迁移:asset_identifier 表、order 表新增 asset_identifier 字段、iot_card.virtual_no NOT NULL 约束
- 资产 Handler/Service/Route 全面重构,支持标识符路由查询与解析
- 新增资产历史订单查询接口,支持跨设备/卡/钱包维度的订单聚合
- 设备与物联卡导入任务强制校验虚拟号,缺失时直接拒绝
- Excel 工具函数优化,前端导入指引文档同步更新
- 归档三个 OpenSpec 提案:asset-identifier-standardization、asset-historical-orders、import-mandatory-virtual-no
- 更新 OpenAPI 文档及相关 DTO

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-07 17:39:36 +08:00
7e489a19fb fix: 角色删除前校验分配情况,禁止删除使用中的角色
角色被删除后,其关联的账号-角色记录未清理,导致 CountByAccountID
统计到孤儿记录,使被分配该角色的账号无法重新分配新角色。

修复方案:删除角色前检查 tb_account_role 和 tb_shop_role,
若仍有账号或店铺持有该角色则返回 1028 错误,强制管理员显式
解除所有分配后再删除,保证数据一致性与审计完整性。
2026-04-07 17:23:49 +08:00
434a8b0349 feat: 轮询系统重构(分片队列 + 停复机统一 + Handler 拆分)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m46s
【核心变更】

1. 停复机逻辑统一(StopResumeService)
   - 新增 EvaluateAndAct 统一入口,封装三条件停复机判断
   - 停机条件:无套餐(no_package) / 流量耗尽(traffic_exhausted) / 未实名(not_realname)
   - 复机条件:stop_reason 合规 + 有套餐且未耗尽 + 已实名或行业卡
   - 修复设备套餐 Bug:hasValidPackage 按 device_id 查套餐,而非仅 iot_card_id
   - 设备维度停复机加幂等锁(Redis SetNX,TTL 30s),防止多卡并发重复调 Gateway

2. Redis 分片队列(PollingQueueManager)
   - 新建 queue_manager.go,封装所有轮询 Redis 操作
   - 16 分片 Sorted Set,Key 格式:polling:shard:{shardID}:queue:{taskType}
   - Lua 脚本原子出队(ZRANGEBYSCORE + 分批 ZREM),消除竞态窗口
   - 新增背压检测:队列深度超 50 万时 Scheduler 跳过该分片
   - RemoveFromAllQueues 覆盖 4 种任务类型(含 protect)

3. Handler 拆分(polling_handler.go 1360行 → 5个专注文件)
   - polling_base.go:共享基类(并发控制/卡缓存/重入队)
   - polling_realname_handler.go:实名采集,实名 0→1 时立即触发复机
   - polling_carddata_handler.go:流量采集,保留跨月边界检测逻辑
   - polling_package_handler.go:套餐采集,委托 EvaluateAndAct 决策
   - polling_protect_handler.go:保护期一致性检查,保护期内强制修正

4. 配置管理(PollingConfigManager)
   - 新建 config_manager.go,从 scheduler.go 提取配置职责
   - 内存缓存 + 5 分钟定时刷新,刷新失败保留原缓存
   - 修复 getCardCondition:停机卡返回 suspended,不再错配 activated 配置

5. 渐进式初始化(CardInitializer)
   - 新建 initializer.go,分批加载(每批 10 万),批次间 sleep 500ms
   - 过滤 enable_polling=false 的卡,初始化完成前 Scheduler 不出队

6. 卡生命周期服务(PollingLifecycleService)
   - 新建 lifecycle_service.go,替代已删除的 callbacks.go 和 api_callback.go
   - OnCardCreated/OnCardEnabled/OnCardStatusChanged 入队前检查 enable_polling

7. Scheduler 精简(1000+行 → 227行)
   - 保留纯调度循环:scheduleLoop + processShardSchedule + enqueueBatch
   - 保留每 10 秒触发套餐过期检测和流量重置
   - 移除所有 DB 操作、配置加载、卡初始化逻辑

8. 轮询管控 API(enable_polling)
   - 新增 PUT /api/admin/assets/:id/polling-status 接口
   - 支持对设备/卡维度开关轮询,关闭后从所有分片队列移除

9. 数据库迁移
   - 000103:tb_device 新增 enable_polling 字段(boolean, NOT NULL, DEFAULT true)
   - 000104:新增 suspended 轮询配置,为 activated 配置补全 protect_check_interval

【文件统计】
- 新增:19 个文件(handler × 5、polling 组件 × 4、迁移 × 3 等)
- 修改:20 个文件(bootstrap 注入、store 接口、monitoring 适配分片等)
- 删除:3 个文件(polling_handler.go、callbacks.go、api_callback.go)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-07 12:27:04 +08:00
10fcc0b3c9 fix: 修复强充场景幂等 key 在微信支付失败时未被清除的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m20s
将 markClientPurchaseCreated 移至 CreateJSAPIPayment 成功之后调用,
确保支付失败时 defer 能可靠地删除幂等 key,避免用户无法重新下单。

原顺序:创建充值单 → markClientPurchaseCreated → CreateJSAPIPayment
修复后:创建充值单 → CreateJSAPIPayment → markClientPurchaseCreated → *created=true

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-07 09:55:06 +08:00
c0c32c62f4 fix: 修复代理充值 Handler 缺少参数验证导致 check constraint 违反的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m18s
payment_method 字段传入中文值「线下转账」时,因 AgentRechargeHandler
未注入 validator 而跳过 oneof 验证,直接写入 DB 触发
chk_agent_recharge_method 约束报错。

- AgentRechargeHandler 注入 validator,Create 方法补加 Struct 验证
- bootstrap/handlers.go 传入 validate 实例
- openapi/handlers.go 补 nil 占位保持编译通过
2026-04-03 09:17:15 +08:00
a8a91fe04e fix: 修复 RealtimeStatus 查询后未回写 DB 导致 Resolve 接口在线状态不同步的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
2026-04-02 15:14:04 +08:00
8980e6d999 fix: 补全三处轮询覆盖缺口
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m11s
问题一:protect 轮询任务从未被调度器初始化入队
PollingConfig 新增 protect_check_interval 字段(NULL=不参与),
调度器 initCardsBatch/initCardPolling/requeueCard 补全 protect 队列
初始化逻辑,IotCard 新增 last_protect_check_at 记录上次检查时间。
迁移文件:000102_add_polling_protect_fields

问题二:设备套餐流量耗尽时绑定卡未被停机
UsageService.checkAndTriggerSuspension 新增 carrier_type=device 分支,
通过 DeviceSimBindingStore.ListByDeviceID 查询绑定卡,对每张卡异步
触发 CheckAndStopCard,同步注入 Bootstrap 依赖。

问题三:主套餐过期后不立即停机,依赖下次轮询兜底
PackageActivationHandler.processExpiredPackage 在 updateCarrierSuspended
Status 后,若载体无后续生效套餐,立即异步调用 CheckAndStopCard(iot_card
类型直接触发,device 类型遍历绑定卡),消除停机延迟窗口。

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-02 14:36:24 +08:00
322ded0012 fix: 修复未启用一次性佣金的系列无法创建授权的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m18s
2026-04-02 14:27:09 +08:00
ecfa417c15 设备调用问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
2026-04-02 11:05:21 +08:00
4770e053ee fix: 修复轮询检测到首次实名时未记录 first_realname_at 的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-02 10:52:58 +08:00
138f08ddd7 fix: 修复平台账号无法访问我的佣金记录和提现记录接口的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m18s
2026-04-02 10:14:44 +08:00
950c0bfb8f fix: 修复已激活在线卡无有效套餐时未被轮询停机的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s
- 新增 activated 轮询配置覆盖已实名+在线的卡(此前仅 real_name 配置)
- HandleCarddataCheck 添加独立停机/复机检查(不依赖流量增量)
- shouldStopCard 对齐 hasAvailablePackage 逻辑,检查待生效+生效中套餐
- 新增复机安全网:流量耗尽停机的卡检测到有效套餐后自动复机
- 卡缓存补充 stop_reason 字段支持复机判断
2026-03-31 20:51:42 +08:00
724967b8b8 fix: 修复日志文件 level 字段包含 ANSI 颜色转义码导致乱码
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
2026-03-31 20:05:12 +08:00
1c6f8ed07b fix: 修复停复机操作未同步轮询卡缓存导致卡无法实际停机的问题
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
多处修改 network_status 的代码只更新了 DB 而未失效轮询缓存(polling:card:{id}),
导致轮询系统持续读取旧缓存值,跳过 Gateway 停机调用。

修复方式:DB 更新后立即 Del 轮询缓存 key,下次轮询自动从 DB 重建。

涉及文件:
- StopResumeService: CheckAndStopCard/resumeSingleCard/ManualStopCard/ManualStartCard
- iot_card/Service: RefreshCardDataFromGateway(通过 OnCardStatusChanged 回调)
- device/Service: StopDevice/StartDevice
2026-03-31 19:59:35 +08:00
58eb047433 fix: 修复手动刷新资产时流量直接覆盖为运营商原始读数的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m6s
RefreshCardDataFromGateway 原先直接将网关返回的累计读数写入
current_month_usage_mb,导致本月已用流量从 312MB 跳变为 29GB。

修复内容:
- 流量计算改为增量逻辑(与轮询 calculateFlowUpdates 一致)
- 补充更新 last_gateway_reading_mb 基准,防止后续轮询增量异常
- 补充跨月检测和运营商重置日窗口判断
- 通过 DataDeductor 回调触发套餐流量扣减(手动刷新也能更新套餐)
2026-03-31 19:50:17 +08:00
045aa4fa3a fix: 修复微信/富友支付回调配置加载错误
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
- WechatPayCallback 移除对永远为 nil 的单例 wechatPayment 的依赖,
  改为动态按 payment_config_id 加载创建订单时所用的精确配置
- PaymentV2Service 新增 VerifyCallback()(v2 XML 验签)和 PeekOrderNo()(不验签预解析)
- FuiouPayCallback 由 GetActiveConfig 改为 GetConfigForCallback,
  通过 ParseNotify 预解析订单号后精确加载配置,防止切换配置后旧订单验签失败
- wechat_config.Service 注入 assetRechargeStore/agentRechargeStore,
  新增 GetConfigForCallback():按订单号前缀查 payment_config_id,
  GetByIDUnscoped 加载配置(含已停用/软删除记录),找不到则回退激活配置
- 修复微信 v2 回调响应格式:从 JSON 改为 XML,避免微信持续重试
2026-03-31 19:17:45 +08:00
69ee754c19 feat: 补全网关停复机调用的关键日志(调用前/成功后均输出 INFO)
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-03-31 19:15:45 +08:00
9d50424edc fix: 修复 access.log response_body 乱码(compress 中间件须在 logger 之前注册)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m7s
2026-03-31 19:06:14 +08:00
302664404b fix: 修复流量轮询 cache 未同步 last_gateway_reading_mb 导致假增量触发停机死循环
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-03-31 19:00:26 +08:00
619f029a5a fix: 修复佣金计算任务载荷二次序列化导致 base64 编码错误
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m11s
问题:EnqueueTask 内部调用 sonic.Marshal,传入 []byte 时会将字节数组
base64 编码为 JSON 字符串,Handler 反序列化时类型不匹配(CommissionCalculationPayload vs string)

根因:order/service.go 的 enqueueCommissionCalculation 预先 Marshal 得到 []byte
后传给 EnqueueTask,导致载荷被二次序列化

修复:直接传入 map,由 EnqueueTask 统一序列化一次

规范同步:
- pkg/queue/client.go 函数注释明确禁止传 []byte
- AGENTS.md 新增「异步任务载荷规范」防止重现
2026-03-31 18:48:17 +08:00
31e147495c fix: 修复提现冻结逻辑错误导致数据库约束违反
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 余额检查从 wallet.Balance 改为 wallet.GetAvailableBalance(),
  防止 frozen_balance 已有值时误判为余额充足
- 冻结余额时只增加 frozen_balance,不再同时减少 balance,
  与系统其他冻结操作保持一致(balance = 总额,frozen 为其子集)
- 修复后 SQL 不再触发 chk_agent_wallet_frozen_balance 约束
2026-03-31 18:41:07 +08:00
ad095d73f3 docs: 同步能力规范 — 新增购买/重置后复机 spec,更新停复机和排队激活规范
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:12:52 +08:00
8b5cd50576 chore: 归档 fix-stop-resume-lifecycle-engine 变更提案
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:12:44 +08:00
8227ab2f20 fix: Bootstrap 注入复机回调至 orderService 和 resetService
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:12:32 +08:00
6457ffa6f9 fix: 支付回调激活套餐后异步触发停机卡自动复机
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:12:24 +08:00
8d69858413 fix: 套餐激活和流量重置后注入并调用复机回调
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:12:16 +08:00
49e8ebed60 fix: 过期套餐检测扩展覆盖 Depleted 状态(status IN (1,2))
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:12:04 +08:00
bb5a5ec6ef fix: stopCards 停机时补充写入 stop_reason 和 stopped_at 字段
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:11:48 +08:00
cf602ab5be fix: ResumeCardIfStopped 扩展 — 支持 device 类型、实名检查、并发锁
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:11:38 +08:00
d7ade821f4 fix: 移除 carrier 表 billing_day 字段(模型 + 数据库迁移)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:11:29 +08:00
7f42d198eb fix: 新增 StopReasonProtectPeriod 常量和 RedisCardResumeLockKey 函数
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-31 18:11:01 +08:00
ebc3e78ba9 修复gateway权限问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m48s
2026-03-31 15:59:54 +08:00
00cae60fe1 chore: 删除卡价格验证 hurl 测试文件
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m14s
2026-03-31 15:26:09 +08:00
67e0840695 fix: 修复钱包充值不支持 wechat_v2 支付渠道导致 1046 报错
当前激活的微信支付配置 provider_type = wechat_v2,但 CreateRecharge
硬编码使用 V3(PowerWeChat)路径,导致初始化失败。

新增 createJSAPIPayOrder 方法,根据 provider_type 路由到对应通道:
- wechat_v2 → NewPaymentV2ServiceFromConfig
- wechat(默认)→ NewPaymentAppFromConfig(V3)

与 client_order.Service.newPaymentProvider 保持相同的路由逻辑。
2026-03-31 15:26:03 +08:00
bc2dbcb416 fix: 修复 V2 微信支付参数全为空字符串的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m17s
extractPaymentResult 只处理 map[string]any,而 V2 自建实现返回
map[string]string,类型断言静默失败导致所有字段为空字符串。
增加对 map[string]string 的处理分支,兼容 V1 和 V2 两种支付渠道。
2026-03-31 14:59:43 +08:00
dc4f3cb7ba feat: C端订单支付拆分 — 创建订单与发起支付分离
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m10s
- CreateOrder 改造:普通下单只建单(无支付参数),强充场景保持原有微信支付一步完成
- 新增 POST /orders/:id/pay 统一支付入口,支持 wallet(钱包)和 wechat(微信 JSAPI)
- 移除 POST /orders/:id/wallet-pay,合并至统一支付接口
- app_type 改为可选字段(强充场景必传,普通下单无需传)
- CreateOrderResponse.pay_config 改为 omitempty(普通下单不返回支付参数)
2026-03-31 12:43:34 +08:00
121462c00f feat: 给 PackageUsage 添加 package_name 快照字段,修复企业卡列表 carrier_name/package_name 为空问题
- 迁移 000099:tb_package_usage 新增 package_name 列,回填历史数据
- PackageUsage model 添加 PackageName 字段(与 DataResetCycle 保持一致的快照设计)
- 4 处 PackageUsage 创建点补充 PackageName 快照(order/service.go + auto_purchase.go)
- EnterpriseCard ListCards:carrier_name 直接取 card.CarrierName,package_name 通过
  DISTINCT ON 批量查当前生效主套餐,单次查询覆盖整页,无 N+1
2026-03-31 12:43:07 +08:00
5b283285a2 fix: 修复微信支付容器内 mkdir 权限错误,新增 wechat_v2 支付渠道
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m11s
- NewPaymentAppFromConfig 加 Log.Stdout=true,禁止 PowerWeChat SDK 在容器
  工作目录创建 wechat/ 日志文件夹(Permission denied 根因)
- 新增 ProviderTypeWechatV2 = wechat_v2 常量,与 wechat(v3) 独立区分
- 新增 PaymentV2Service:直连统一下单 v2 接口,XML + MD5 签名
- newPaymentProvider 按 ProviderType 分支路由到对应版本
2026-03-31 12:12:13 +08:00
4e65cf95d1 fix: 修复企业端用户所有接口返回403的问题 — AdminAuth中间件白名单遗漏Enterprise类型
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m15s
2026-03-31 10:41:31 +08:00
2b408230a6 skill
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s
2026-03-31 10:00:01 +08:00
48ad0844ae fix: gendocs panic — config.Get() nil 守卫防止未初始化时访问 Logging.Development
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
2026-03-31 09:56:25 +08:00
2ef8b8a705 fix: 修复分页字段/角色DTO/套餐详情/批量分配Bug,新增C端测试登录接口和Hurl价格验证测试
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 修复B-2:27个分页DTO字段名统一(list→items, page_size→size,删除total_pages)
- 修复B-1:角色接口统一返回DTO(id小写,消除GORM大写字段名)
- 修复C-1:套餐详情接口(GET /packages/:id)补充代理佣金字段
- 修复C-2:批量分配已存在记录由500改为静默跳过
- 修复C-3:创建系列授权响应在事务提交后构建,packages数组不再为空
- 新增C端开发测试登录接口(POST /api/c/v1/auth/dev-login,仅logging.development=true时生效)
- 新增Hurl测试:card-price-verification-flow.hurl(导入卡→分配→C端价格验证)
- 新增测试Excel数据文件和dev.env测试变量
2026-03-31 09:54:08 +08:00
33e7f99fdc fix: 修复数据清理预览接口 panic 及路由文档路径翻倍问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
1. CountOldRecords 改用 GORM .Scan() 替代 .Row().Scan(),避免查询不存在的表时 nil pointer panic
2. polling_cleanup 路由注册的 basePath 参数去掉多余拼接,修复生成的 OpenAPI 文档路径翻倍
2026-03-30 15:09:14 +08:00
06ee422e34 fix: 富友验签失败 — 补全3.3接口所有非reserved字段(空值字段也必须参与签名)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m7s
2026-03-30 13:48:45 +08:00
30d6743310 fix: 富友验签错误 — 签名原文必须包含空值字段(与富友文档签名实例一致)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m9s
2026-03-30 13:21:04 +08:00
a97462dd69 fix: 支付失败时幂等标记未清理导致用户无法重试 — *created=true移到支付成功后
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m9s
2026-03-30 13:06:45 +08:00
d56b841443 fix: 富友回调应答XMLName缺失 + term_ip自动探测服务器IP替代硬编码
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
2026-03-30 12:46:50 +08:00
8296742cce fix: 富友预下单补充必填字段 txn_begin_ts(交易起始时间)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m6s
2026-03-30 12:23:50 +08:00
0c82ae2b73 fix: 修复富友预下单报文格式错(1014) — XML根元素、小程序tradeType、termIP三处问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m27s
2026-03-30 11:57:46 +08:00
f339fb1987 fix: 资产钱包自动创建机制 — 修复C端购买时钱包不存在报错
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
- client_order: 新增 getOrCreateWallet 兜底,钱包不存在时自动创建
- device_import: 设备导入事务内同步创建设备钱包
- iot_card_import: IoT卡批量导入后批量创建卡钱包
- queue/handler: 传递 AssetWalletStore 给两个导入 handler
- migration 000098: 为存量IoT卡和设备补建资产钱包
2026-03-30 11:37:41 +08:00
40809d11c5 修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
2026-03-30 10:20:16 +08:00
50dbc52432 fix: C端资产详情设备补充已连接数(client_number)字段
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Gateway sync-info 返回的 client_number 字段未在整条数据链路中传递,
导致 C 端资产详情的设备实时状态缺少已连接客户端数。

修改范围:
- Gateway 模型 SyncDeviceInfoResp 新增 ClientNumber
- B 端 DTO DeviceGatewayInfo 新增 ClientNumber
- C 端 DTO DeviceRealtimeInfo 新增 ClientNumber
- 两层映射函数同步补充字段传递
2026-03-30 10:18:30 +08:00
cebcada950 refactor: 流量系统重构 — 增量累加算法 + 日粒度缓冲 + 旧详单清理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m16s
核心改造:
- 增量算法:流量计算从覆盖式改为增量累加(gateway - lastReading),支持上游运营商重置检测
- 日流量缓冲:insertDataUsageRecord 改为 Redis INCRBYFLOAT,每日凌晨落盘到 tb_card_daily_usage
- 运营商:新增 data_reset_day 字段(联通=27,其余=1)
- IoT卡:新增 last_gateway_reading_mb 字段存储上次网关读数
- 查询层:新建 TrafficQueryService 合并 Redis(今日)+ DB(历史)数据源
- 清理:删除 DataUsageRecord model/store,移除 polling_handler 旧引用

迁移:000094-000097(carrier字段、iot_card字段、数据初始化、日流量表)
2026-03-30 09:59:30 +08:00
f5dd2ce4ab fix: Gateway 响应类型兼容 — 新增 FlexBool/FlexInt 处理设备返回的字符串类型字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m4s
部分设备厂商(如华为)的 Gateway 响应中 bool/int 字段返回为字符串(wifi_enabled: "1"、battery_level: "30"),
导致 sonic 严格类型检查反序列化失败,整个设备实时数据丢失。

- 新增 FlexBool 类型:兼容 true/false、"1"/"0"、"true"/"false"
- 新增 FlexInt 类型:兼容 30、"30"、null
- SyncDeviceInfoResp 所有 bool/int 字段改用 Flex 类型
- 更新 B 端和 C 端 DTO 映射函数的类型转换
2026-03-28 18:24:17 +08:00
02b10f87cf feat: 新增退款管理模块 — 完整的退款审批流程、佣金回扣和资产重置
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
新增退款申请全生命周期管理:创建/列表/详情/审批通过/拒绝/退回/重新提交
审批通过后异步执行佣金全额回扣(扣减各代理佣金钱包)和资产重置(套餐失效+停机+世代重置)
新增 tb_refund_request 表(迁移 000093)、RefundRequest Model、8 个 DTO
新增 Store/Service/Handler/路由注册,仅平台用户可访问
2026-03-28 17:57:52 +08:00
5e64c64ce3 修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
2026-03-28 17:12:29 +08:00
a2fd1dfd62 fix: 佣金修正接口 DTO 补充 path 参数声明,修复 OpenAPI 生成 panic
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m59s
2026-03-28 17:01:06 +08:00
623a622298 feat: 业务逻辑补全 — 佣金待审记录、C端订单重构、支付抽象、富友支付、卡设备状态联动
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
F-1: 佣金链断裂时创建 status=99 零额待审记录,新增修正接口 POST /commission-records/:id/resolve
F-4: C端订单查询从 Handler 迁移至 Service 层,移除 SkipPermissionCtx
J-1: 富友支付 JSAPI/MiniApp 预下单实现,回调补全签名验证
J-2: 平台钱包代购支持(buyerType 为空时使用资产所属代理钱包)
J-3: 套餐激活后自动更新卡/设备 status=3,最后套餐过期后更新 status=4
支付抽象: 引入 PaymentProvider 接口 + 微信/富友适配器,CreateOrder 支持多支付渠道
修复: 设备/IoT卡响应 DTO 移除 omitempty,空值字段返回 null 而非省略
2026-03-28 16:57:39 +08:00
65e461eff7 fix: 代码质量清理 — 统一C端支付枚举、修复静默错误、注入Worker回调、移除废弃同步代码
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m6s
- C端订单支付状态直接使用管理端统一枚举(1/2/3/4),移除冗余映射函数
- 资产查询绑定卡/套餐查询失败时记录Warn日志而非静默忽略
- Worker bootstrap注入停复机回调(流量耗尽停机、套餐激活复机)
- 删除废弃的SIM状态同步服务和任务处理器
- 新增开发环境数据清理脚本(full/soft/table三种模式)
2026-03-28 15:27:59 +08:00
e81d3a58ad docs(04): capture phase context
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m6s
2026-03-28 14:24:25 +08:00
da8aedc96b docs(phase-03.1): evolve PROJECT.md after phase completion 2026-03-28 13:42:29 +08:00
9995dcd0a3 docs(phase-03.1): complete phase execution 2026-03-28 13:41:26 +08:00
f0f21e52f1 docs: 重新生成 OpenAPI 文档,补全 Phase 03.1 新增字段 2026-03-28 13:32:48 +08:00
a8becf55ff chore: 更新 STATE.md 记录 Phase 03.1 完成 2026-03-28 13:24:39 +08:00
f9da1a937f docs(03.1-01): 完成 Phase 03.1 Plan 01 执行总结
- 创建 03.1-01-SUMMARY.md:记录 3 个任务完成情况、6 个文件修改、SwitchMode 类型偏差修复
- 更新 STATE.md:追加执行日志、补充 B/C 端独立 DTO 结构决策
2026-03-28 13:23:43 +08:00
a202b2da8a feat(03.1-01): 建立设备实时 Gateway 调用链路,替换 mock 数据
- GetRealtimeStatus() device case 新增 Gateway sync-info 实时调用(IMEI/SN 优先,gatewayClient nil 安全)
- 新增 mapSyncRespToDeviceGatewayInfo() 将 SyncDeviceInfoResp 映射为 B 端 DeviceGatewayInfo DTO
- 新增 strPtr() 辅助函数(空字符串→nil,防止 JSON 出现大量空字符串字段)
- client_asset.go 删除 buildMockDeviceRealtime(),改为调用 assetService.GetRealtimeStatus() 获取真实数据
- 新增 mapDeviceGatewayInfoToClientInfo() 将 B 端 DeviceGatewayInfo 映射为 C 端 DeviceRealtimeInfo
- Gateway 调用失败时 DeviceRealtime 返回 null,不阻断主流程
2026-03-28 13:21:02 +08:00
3b54850f1b feat(03.1-01): 修复静态 DTO 映射缺口
- toDeviceResponse() 补全 5 个 DB 缓存字段(OnlineStatus/LastOnlineTime/SoftwareVersion/SwitchMode/LastGatewaySyncAt)
- ListBindings() 补全 IsCurrent 字段映射
- buildDeviceResolveResponse() 补全 isCurrentMap + BoundCardInfo.IsCurrent + 5 个 DB 缓存字段
- GetRealtimeStatus() device case 补全 isCurrentMap + BoundCardInfo.IsCurrent
2026-03-28 13:19:09 +08:00
00dba42a63 feat(03.1-01): 补全 Gateway SyncDeviceInfoResp 缺失字段及 B 端 DeviceGatewayInfo DTO
- SyncDeviceInfoResp 新增 Rsrp/Rsrq/Sinr/WifiPassword/IPAddress/WANIP/LANIP/MaxClients/ConnectTime/Status/IMSI/ULStats/DLStats/LimitSpeed/SyncInterval 共 15 个字段
- asset_dto.go 新增 DeviceGatewayInfo B 端实时状态结构体(49 字段,全部含 description tag)
- AssetRealtimeStatusResponse 新增 DeviceRealtime *DeviceGatewayInfo 字段
- AssetResolveResponse 新增 5 个设备 DB 缓存字段(online_status/last_online_time/software_version/switch_mode/last_gateway_sync_at)
2026-03-28 13:17:52 +08:00
3450c20236 docs(03.1): 创建 Phase 03.1 计划(设备读取链路修复 + DTO 映射补全) 2026-03-28 13:12:42 +08:00
debe8f6edc docs(03.1): 补充 D-12 AssetResolveResponse 5字段扩展决策 2026-03-28 13:04:35 +08:00
d0ef3bb712 修复错误的状态码 2026-03-28 12:54:04 +08:00
8826d95516 docs(state): record phase 03.1 context session 2026-03-28 12:38:15 +08:00
8af5d1a045 docs(03.1): capture phase context 2026-03-28 12:38:02 +08:00
ff62ea457a docs(phase-03): evolve PROJECT.md after phase completion 2026-03-28 11:45:58 +08:00
71b948f22b docs(phase-03): complete phase execution 2026-03-28 11:45:22 +08:00
129a38f942 docs(03-02): 完成设备体系 sync-info 接入计划(DEVICE-04)
- 新增 03-02-SUMMARY.md,记录 updateDeviceFromSyncInfo 实现细节
- STATE.md:Phase 3 完成标记,追加 DEVICE-04 决策,Next Action 更新为 Phase 4
- ROADMAP.md:Phase 3 进度更新为 Complete(2/2 plans)
- REQUIREMENTS.md:DEVICE-04 标记完成
2026-03-28 11:40:04 +08:00
15dbf8dc66 feat(03-02): Asset Service 接入 Gateway sync-info(DEVICE-04)
- Service 结构体新增 gatewayClient 字段,New() 追加 gatewayClient 参数
- Refresh device 分支在绑定卡刷新后调用 SyncDeviceInfo,nil guard + Warn 日志不阻断主流程
- 新增 updateDeviceFromSyncInfo 私有函数:更新 5 个 device 字段 + 调用 UpdateIsCurrentByDeviceID
- bootstrap/services.go asset.New() 调用追加 deps.GatewayClient
- go build ./... 零新增编译错误
2026-03-28 11:38:14 +08:00
ba1886c314 feat(03-02): DeviceSimBindingStore 新增 UpdateIsCurrentByDeviceID 方法
- 事务两步原子更新:先全部清空 is_current=false,再按 ICCID 设 true
- 通过子查询定位 iot_card_id,避免跨表 JOIN 更新
- currentIccid 为空时仅清空,不做第二步设置
2026-03-28 11:36:40 +08:00
8819c94322 docs(03-01): 完成设备体系数据基础层计划
- 创建 03-01-SUMMARY.md
- 更新 STATE.md(进度推进至 Plan 2)
- 更新 ROADMAP.md(Phase 3 进行中)
- 标记 REQUIREMENTS.md(DEVICE-01/02/03 完成)
2026-03-28 11:34:38 +08:00
825def88a4 feat(03-01): Gateway SyncDeviceInfo 方法 + DTO 字段扩展
- gateway/models.go:新增 SyncDeviceInfoReq / SyncDeviceInfoResp 结构定义
- gateway/device.go:新增 SyncDeviceInfo() 方法,POST /device/sync-info
- device_dto.go:DeviceResponse 新增 5 个 sync-info 同步字段;DeviceCardBindingResponse 新增 is_current
- asset_dto.go:BoundCardInfo 新增 is_current 字段
2026-03-28 11:33:01 +08:00
42b884aaf8 feat(03-01): DB 迁移扩展设备字段 + 更新模型
- 新增迁移 000090:tb_device 扩展 5 个 Gateway sync-info 字段(online_status / last_online_time / software_version / switch_mode / last_gateway_sync_at)
- 新增迁移 000091:tb_device_sim_binding 新增 is_current 字段(当前使用卡标识)
- 更新 Device 模型新增对应 5 个字段
- 更新 DeviceSimBinding 模型新增 IsCurrent 字段
2026-03-28 11:31:16 +08:00
8a4c6f3cee docs(03-device-system): 创建 Phase 3 设备体系完善计划(2个Plan) 2026-03-28 11:24:31 +08:00
ce6aa62fd6 docs(03): capture phase context 2026-03-28 11:16:02 +08:00
4ff5dcf55e docs(roadmap): 修复 Phase 2 状态标记 — 勾选复选框、更新 Progress 表格
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m9s
2026-03-28 10:56:21 +08:00
d3eaac0eb0 docs(state): 修复 STATE.md 状态混乱 — 同步至 Phase 2 完成后实际状态 2026-03-28 10:54:53 +08:00
8ace759200 docs(phase-02): evolve PROJECT.md after phase completion — B/C/G/H 方案全部标记 Validated 2026-03-28 10:53:37 +08:00
5432fb0513 docs(phase-02): 完成 Phase 2 执行 PERM/FIN/REALNAME/POLL 14项修复 2026-03-28 10:52:47 +08:00
e1ec024904 docs(02-01): 完成 PERM+FIN+POLL 9项修复计划 PERM-01/02 FIN-01/02/03 POLL-01/02/03/04 2026-03-28 10:46:10 +08:00
754bbcdf3b fix(fin): 同步更新 openapi handlers 中 CommissionWithdrawalHandler 签名
- NewCommissionWithdrawalHandler 新增了 validator 参数,同步更新文档生成器调用处
2026-03-28 10:43:32 +08:00
47aa5f8d25 fix(poll): Series 删除前检查关联套餐 POLL-04
- package_store.go 新增 CountBySeriesID 方法,统计指定系列下的活跃套餐数量
- package_series/service.go Service 注入 packageStore 字段,Delete 前调用 CountBySeriesID 检查
- bootstrap/services.go 更新 PackageSeries 服务初始化,传入 PackageStore
2026-03-28 10:42:58 +08:00
facc766993 docs(02-02): 完成实名激活架构重构计划 REALNAME-01~05
- 新增 02-02-SUMMARY.md
- 更新 STATE.md 进度(plan 2/2)
- 更新 ROADMAP.md 进度
- 标记 REALNAME-01~05 需求完成
2026-03-28 10:42:04 +08:00
8553a46143 fix(poll): GetPackages 接口新增分页 POLL-03
- 新增 AssetPackagesResult DTO(含 total/page/page_size/items 字段)
- GetPackages 签名新增 page/pageSize 参数,默认 page=1、pageSize=50、最大 100
- admin/asset.go:Packages 从 Query 参数读取 page/page_size 传入 Service
- client_asset.go 调用处同步更新,使用默认分页参数
2026-03-28 10:41:20 +08:00
4be473c248 fix(realname): 移除 auto_purchase.go 中废弃的 EnableRealnameActivation 引用
- C 端充值触发的自动购包不需要等实名(前置检查已完成)
- 删除对已废弃 EnableRealnameActivation 字段的引用
2026-03-28 10:40:08 +08:00
8a531237c3 fix(poll): gatewayClient=nil 时停复机返回错误而非成功 POLL-02
- stopCardWithRetry: gatewayClient==nil 时返回业务错误而非 nil(静默跳过会假装停机成功)
- resumeCardWithRetry: 同上修复
2026-03-28 10:39:47 +08:00
922a3d0b49 fix(poll): polling:protect 接入调度器 POLL-01
- processSchedule 中新增 TaskTypePollingProtect 的 processManualQueue 和 processTimedQueue 调用
- 保护期一致性检查任务现在会被定期调度处理
2026-03-28 10:39:18 +08:00
60b43bb29e fix(realname): ActivateByRealname 按 ExpiryBase 选择计时基准 REALNAME-04
- from_purchase:用 PackageUsage.CreatedAt(购买时刻)作为激活时间起点
- from_activation(默认):用实名被触发的当前时刻作为激活时间起点
2026-03-28 10:39:00 +08:00
bdac4ab7b2 fix(fin): 激活配置并发行锁保护 FIN-03
- commission_withdrawal_setting/service.go: 激活事务内首步加 FOR UPDATE 行锁,防止并发时出现多条 is_active=true
- wechat_config_store.go: ActivateInTx 激活前同样加行锁保护
2026-03-28 10:38:53 +08:00
0ebb3d75f3 fix(realname): C 端实名检查改为按卡类型判断 REALNAME-03
- 删除 packagesNeedRealname 函数(依赖已废弃的 EnableRealnameActivation)
- 改为按 assetInfo.CardCategory==normal 判断是否需要实名
- 行业卡(industry)绕过实名检查,普通卡(normal)需要实名
2026-03-28 10:38:44 +08:00
7dd55630dc fix(realname): activateMainPackage 三维决策重构 REALNAME-02
- 替换 EnableRealnameActivation 布尔判断为三维决策逻辑
- 行业卡(card_category=industry)永远直接激活
- C 端购买(BuyerTypePersonal)前置实名检查已完成,直接激活
- 后台囤货按 ExpiryBase 决定:from_activation 等实名,from_purchase 立即激活
2026-03-28 10:38:24 +08:00
9ee2f0b466 fix(fin): 提现拒绝补 remark 必填校验 FIN-02
- CommissionWithdrawalHandler 新增 validator 字段
- RejectWithdrawal 在 BodyParser 后补充 validator.Struct 参数验证,确保 remark 必填
2026-03-28 10:37:54 +08:00
244a2742c1 fix(realname): DTO 更新 expiry_base 替换 enable_realname_activation REALNAME-05
- package_dto.go: CreatePackageRequest/UpdatePackageRequest/PackageResponse 三处替换
- service.go: 同步更新 DTO 映射逻辑使用 ExpiryBase
2026-03-28 10:37:51 +08:00
8f62496250 fix(fin): 补全提现审批人字段 FIN-01
- Approve updates map 补充 approved_by 和 approved_at 字段,确保审批人信息正确填充
2026-03-28 10:36:33 +08:00
344850fe1d fix(realname): DB 迁移 + Model + Store 清理 REALNAME-01
- 新增迁移 000089: 移除 enable_realname_activation,添加 expiry_base 字段
- package.go: 删除 EnableRealnameActivation 字段,新增 ExpiryBase 字段
- package_store.go: 移除两步写入特殊处理,改为直接一步 Create
2026-03-28 10:36:31 +08:00
2a92ab64af fix(perm): 修复企业账号资产权限边界 PERM-01/02
- PERM-01: 删除 Resolve 接口对企业账号的错误拦截(企业账号应可查询资产)
- PERM-02: 在 Refresh 接口开头新增企业账号拦截(企业账号只读,不允许主动触发运营商刷新)
2026-03-28 10:36:17 +08:00
62eb43a1cf docs(02-perm-fin-realname-poll): 创建 Phase 2 执行计划(2 个 Plan) 2026-03-28 10:29:50 +08:00
84aca2fd63 docs(02): capture phase context 2026-03-28 10:11:04 +08:00
c083596ef6 docs(phase-01): evolve PROJECT.md after phase completion — move CRITICAL-01~08 to Validated
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m18s
2026-03-28 09:49:15 +08:00
fb14c51fc2 docs(phase-01): complete phase execution — verification passed, all CRITICAL-01~08 satisfied 2026-03-28 09:47:54 +08:00
dbeed92ed9 docs(01-03): 完成充值回调自动购包链路修复 Plan 执行总结
- 创建 01-03-SUMMARY.md(CRITICAL-04 + CRITICAL-05 修复记录)
- 更新 STATE.md(Plan 计数推进至 4/5,progress 100%)
- 更新 ROADMAP.md(Phase 1 进度更新)
- 标记 CRITICAL-04、CRITICAL-05 需求已完成
2026-03-27 23:33:47 +08:00
030425be33 feat(01-03): AutoPurchaseHandler 注入 asynqClient,触发佣金并注册处理器(CRITICAL-04/05)
- AutoPurchaseHandler struct 新增 asynqClient *asynq.Client 字段
- NewAutoPurchaseHandler() 新增 asynqClient 参数
- ProcessTask 事务成功后入队 TaskTypeCommission(事务外,防止一致性问题)
- pkg/queue/handler.go 新增 registerAutoPurchaseHandler() 方法
- RegisterHandlers() 调用 registerAutoPurchaseHandler()
2026-03-27 23:31:49 +08:00
0ed804c4b3 docs(01-02): 完成 PollingHandler 注入 asynq.Client 修复断链计划摘要 2026-03-27 23:30:47 +08:00
20b4b6d6bf fix(01-02): 修复实名激活任务断链,为 PollingHandler 注入 asynq.Client
- PollingHandler struct 新增 asynqClient *asynq.Client 字段
- NewPollingHandler 构造函数新增 asynqClient 参数(redis 之后,gatewayClient 之前)
- triggerFirstRealnameActivation 改用 asynqClient.EnqueueContext,删除 RPush 降级方案
- 删除 _ = task 废弃代码
- registerPollingHandlers 传入 h.asynqClient 参数
2026-03-27 23:29:12 +08:00
9a4d87a0d4 feat(01-03): 充值回调触发自动购包任务入队(CRITICAL-04)
- recharge.Service struct 新增 queueClient *queue.Client 字段
- recharge.New() 新增 queueClient 参数
- HandlePaymentCallback 事务成功后,LinkedPackageIDs 非空时入队 TaskTypeAutoPurchaseAfterRecharge
- bootstrap/services.go 的 recharge.New() 传入 deps.QueueClient
2026-03-27 23:29:05 +08:00
0df992c98a docs(01-04): 完成 sellerCostPrice CRITICAL-06 修正计划摘要 2026-03-27 23:25:48 +08:00
f74b7da25f fix(01-04): 修正场景5代理代购下级的sellerCostPrice赋值
- 三处代理代购下级分支(wallet场景子场景2.2)均将 sellerCostPrice = buyerCostPrice 改为 sellerCostPrice = operatorCostPrice
- buyerCostPrice 是下级成本价,operatorCostPrice 是操作方(代理自己)的成本价
- 使用 buyerCostPrice 会导致佣金差额为 0,所有上级代理无法获得应有佣金
- 修复涉及三个函数(普通下单/后台下单/H5下单),行号分别约在 264/551/813
2026-03-27 23:24:16 +08:00
6de830fcc4 docs(01-01): 完成实名状态常量统一计划执行总结
- 创建 01-01-SUMMARY.md(CRITICAL-01 + CRITICAL-02 完成)
- STATE.md: 更新进度至 Plan 3/5,记录执行指标
- ROADMAP.md: 更新 Phase 1 计划进度(2/5 完成)
- REQUIREMENTS.md: 标记 CRITICAL-01、CRITICAL-02 已完成
2026-03-27 22:58:05 +08:00
da50a35a62 fix(01-01): 修复 C 端实名校验使用常量(CRITICAL-02)
- client_order/service.go: 实名校验 assetInfo.RealNameStatus != 1 改为常量比较
- client_realname.go: 实名状态检查 == 1 改为 constants.RealNameStatusVerified
- go build ./... 编译通过,无遗留硬编码 != 1 / == 1 的实名判断
2026-03-27 22:54:50 +08:00
1fd0072f69 fix(01-01): 统一实名状态常量(CRITICAL-01 核心)
- polling_handler.go: parseRealnameStatus 不再返回硬编码 2,改为 constants.RealNameStatusVerified(=1)
- polling_handler.go: isFirstRealname 判断改用常量比较,不再使用 == 2
- polling_handler.go: HandleProtectConsistencyCheck 注释更正为 real_name_status=1
- iot_card.go: RealNameStatus gorm comment 统一为 0-未实名 1-已实名(去除行业卡说明)
- asset_dto.go: RealNameStatus description 更正为 0未实名 1已实名(去除错误的 1实名中 2已实名)
- scheduler.go: getCardCondition 改用常量替代硬编码 0/1/2 判断
- iot_card/service.go: parseGatewayRealnameStatus 注释更正,返回值改为常量
2026-03-27 22:53:30 +08:00
bfcea1c18f docs(01-05): 完成 Plan 05 执行摘要,更新 STATE/ROADMAP/REQUIREMENTS
- 创建 01-05-SUMMARY.md(CRITICAL-07 + CRITICAL-08 修复记录)
- STATE.md 进度推进至 plan 2/5(20%)
- ROADMAP.md 更新 Phase 1 进度(1/5 SUMMARY 已完成)
- REQUIREMENTS.md 标记 CRITICAL-07、CRITICAL-08 为完成
2026-03-27 22:52:27 +08:00
db166807c7 fix(01-05): 提现冻结并发校验缺失(CRITICAL-08)
- 冻结余额改为 result := tx...Updates(),分离结果对象
- 检查 result.Error 处理数据库错误
- 检查 result.RowsAffected == 0 防止并发余额不足时仍创建提现单
- RowsAffected == 0 时返回 errors.New(CodeInsufficientBalance, ...)
2026-03-27 22:50:18 +08:00
809cb2b8a8 fix(01-05): 设备导入补充 IMEI 字段(CRITICAL-07)
- DeviceRow struct 新增 IMEI string 字段
- buildDeviceColumnIndex 支持 imei/设备imei/imei号 列名映射
- ParseDeviceExcel 解析数据行时填充 row.IMEI
- device_import.go processBatch 创建 model.Device 时赋值 IMEI: row.IMEI
2026-03-27 22:49:40 +08:00
f48f11beb4 docs(01-p0): 创建 Phase 1 P0 紧急修复执行计划(5 个 Plan,涵盖 CRITICAL-01~08) 2026-03-27 22:35:28 +08:00
5862018e7c docs(01): capture phase context 2026-03-27 21:52:09 +08:00
dc9bf0c9b7 docs: create roadmap (6 phases) 2026-03-27 20:18:06 +08:00
7f2e978288 docs: define v1 requirements 2026-03-27 19:07:58 +08:00
3078bc71c8 docs: add research findings 2026-03-27 19:04:17 +08:00
cf59380784 docs: initialize project 2026-03-27 18:50:18 +08:00
876151ec1c 修复
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m22s
2026-03-23 09:49:30 +08:00
c10b70757f fix: 资产信息接口 device_realtime 字段返回固定假数据,避免前端因 nil 报错
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m58s
Gateway 同步接口尚未对接,临时为设备类型资产返回 mock 数据,
后续对接后搜索 buildMockDeviceRealtime 替换为真实数据
2026-03-21 14:42:48 +08:00
4d1e714366 fix: 补齐迁移 000076 遗漏的列名重命名(card_wallet_id → asset_wallet_id)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m52s
迁移 000076 只将表名从 card_wallet 改为 asset_wallet,但遗漏了表内
card_wallet_id 列的重命名,导致 Model 中 column:asset_wallet_id 与数据库
实际列名不匹配,所有涉及该字段的 INSERT/SELECT 均报错 2002。

影响范围:
- tb_asset_recharge_record.card_wallet_id → asset_wallet_id
- tb_asset_wallet_transaction.card_wallet_id → asset_wallet_id
2026-03-21 14:30:29 +08:00
d2b765327c 完整的字段返回
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m52s
2026-03-21 13:41:44 +08:00
7dfcf41b41 fix: 修复卡类型资产绑定键错误导致归属校验永远失败
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m48s
resolveAssetBindingKey 对卡类型错误地返回 card.ICCID 作为绑定键,
但归属校验 isCustomerOwnAsset 使用 card.VirtualNo 比对,二者不一致
导致所有卡资产的 C 端接口返回 403 无权限。

修复:卡类型绑定键改为 card.VirtualNo,与设计文档一致。
附带数据迁移修正已有的错误绑定记录。
2026-03-21 11:33:57 +08:00
ed334b946b refactor: 清理重构遗留的死代码
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- personal_customer.Service: 删除已迁移到 client_auth 的死方法
  (GetProfile/SendVerificationCode/VerifyCode),移除多余的
  verificationService/jwtManager 依赖
- 删除 internal/service/customer/ 整个目录(零引用的早期残留)
2026-03-21 11:33:06 +08:00
95b2334658 feat: 资产套餐历史接口新增 package_type 和 status 筛选条件
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m10s
GET /api/c/v1/asset/package-history 支持可选参数:
- package_type: formal(正式套餐) / addon(加油包)
- status: 0(待生效) / 1(生效中) / 2(已用完) / 3(已过期) / 4(已失效)
不传则返回全部,保持向后兼容。
2026-03-21 11:01:21 +08:00
da66e673fe feat: 接入短信服务,修复 SMS 客户端 API 路径
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- cmd/api/main.go: 新增 initSMS() 初始化短信客户端并注入 verificationService
- pkg/sms/client.go: 修复 API 路径缺少 /sms 前缀(/api/... → /sms/api/...)
- docker-compose.prod.yml: 添加线上短信服务环境变量
2026-03-21 10:51:43 +08:00
284f6c15c7 fix: 修复个人客户设备绑定查询使用已废弃的 device_no 列名
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m4s
数据库列已重命名为 virtual_no,但 Store 层 3 处原始 SQL 仍使用旧列名 device_no,
导致小程序登录时查询客户资产绑定关系报 column device_no does not exist。
2026-03-20 18:20:24 +08:00
55918a0b88 fix: 修复 C 端公开路由被认证中间件拦截的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m51s
Fiber 的 Group.Use() 在路由表中注册全局 USE 处理器,不区分 Group 对象。
原代码先调用 authProtectedGroup.Use() 再注册公开路由,导致 verify-asset、
wechat-login、miniapp-login、send-code 四个无需认证的接口被拦截返回 1004。

修复方式:公开路由直接注册在 router 上且在任何 Use() 之前,
利用 Fiber 按注册顺序匹配的机制确保公开路由优先命中。
2026-03-20 18:01:12 +08:00
d2494798aa fix: 修正停复机接口错误码,网关失败不再返回模糊的内部服务器错误
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s
- 单卡停复机:网关错误从 CodeInternalError(2001) 改为 CodeGatewayError(1110),前端可看到具体失败原因
- 单卡停复机:DB 更新裸返 GORM error 改为 CodeDatabaseError(2002) 包装
- 设备复机:全部卡失败时错误码从 CodeInternalError 改为 CodeGatewayError
2026-03-19 18:37:03 +08:00
1439 changed files with 143869 additions and 16849 deletions

30
.agents/rules/ponytail.md Normal file
View File

@@ -0,0 +1,30 @@
# Ponytail, lazy senior dev mode
You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
Before writing any code, stop at the first rung that holds:
1. Does this need to be built at all? (YAGNI)
2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
3. Does the standard library already do this? Use it.
4. Does a native platform feature cover it? Use it.
5. Does an already-installed dependency solve it? Use it.
6. Can this be one line? Make it one line.
7. Only then: write the minimum code that works.
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
Rules:
- No abstractions that weren't explicitly requested.
- No new dependency if it can be avoided.
- No boilerplate nobody asked for.
- Deletion over addition. Boring over clever. Fewest files possible.
- Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
- Question complex requests: "Do you actually need X, or does Y cover it?"
- Pick the edge-case-correct option when two stdlib approaches are the same size, lazy means less code, not the flimsier algorithm.
- Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.

View File

@@ -0,0 +1,78 @@
---
name: ask-matt
description: Ask which skill or flow fits your situation. A router over the skills in this repo.
disable-model-invocation: true
---
# Ask Matt
You don't remember every skill, so ask.
A **flow** is a path through the skills. Most paths run along one **main flow**, and two **on-ramps** merge onto it. Everything else is standalone, or a vocabulary layer that runs underneath.
## The main flow: idea → ship
The route most work travels. You have an idea and want it built.
1. **`/grill-with-docs`** — sharpen the idea by interview. Start here when you **have a codebase**: it's stateful, retaining what it learns in `CONTEXT.md` and ADRs. (No codebase? Use `/grill-me` — see Standalone. Both run the same `/grilling` primitive; `grill-with-docs` is the one that leaves a paper trail.)
2. **Branch — can you settle every question in conversation?** If a question needs a runnable answer (state, business logic, a UI you have to see), detour through a prototype, bridged by **`/handoff`** in both directions (see Crossing sessions):
- **`/handoff`** out, then open a fresh session against that file,
- **`/prototype`** to answer the question with throwaway code,
- **`/handoff`** back what you learned, and reference it from the original idea thread.
3. **Branch — is this a multi-session build?**
- **Yes** → **`/to-spec`** (turn the thread into a spec), then **`/to-tickets`** to split it into tracer-bullet tickets, each declaring its **blocking edges**. On a local tracker that's one file per ticket under `.scratch/<feature>/issues/`, worked blockers-first by hand; on a real tracker the edges become native blocking links, so any ticket whose blockers are done can be grabbed — kick off **`/implement`** per ticket, **clearing context between each one**.
- **No** → **`/implement`** right here, in the same context window.
Either way, **`/implement`** builds each issue by driving **`/tdd`** internally — one red-green slice at a time — then closes out by running **`/code-review`**, a two-axis review (Standards + Spec) of the diff, before committing. Reach for **`/tdd`** on its own when you just want to build a concrete behaviour test-first without a full spec, and **`/code-review`** on its own whenever you want to review a branch or PR against a fixed point.
### Context hygiene
Keep steps 13 in **one unbroken context window** — don't compact or clear until after `/to-tickets` — so the grilling, spec, and tickets all build on the same thinking. Each `/implement` then starts fresh, working from the ticket.
The limit on this is the **[smart zone](https://www.aihero.dev/ai-coding-dictionary/smart-zone)**: the window (~120k tokens on state-of-the-art models) within which the model still reasons sharply. If a session approaches it before `/to-tickets`, don't push on degraded — `/handoff` and continue in a fresh thread.
## On-ramps
A starting situation that generates work, then merges onto the main flow.
- **Bugs and requests piling up** → **`/triage`**. It moves issues through triage roles and produces agent-ready issues, which **`/implement`** later picks up.
Triage is only for issues **you didn't create** — bug reports, incoming feature requests, anything that arrives raw. Tickets that `/to-tickets` produced are already agent-ready, so **don't triage them**.
- **Something's broken** → **`/diagnosing-bugs`**. For the hard ones: the bug that resists a first glance, the intermittent flake, the regression that crept in between two known-good states. It refuses to theorise until it has a **tight feedback loop** — one command that already goes red on *this* bug — then fixes with a regression test. Its post-mortem hands off to **`/improve-codebase-architecture`** when the real finding is that there's no good seam to lock the bug down.
- **A huge, foggy effort — a greenfield project or a huge feature build, too big for one session** → **`/wayfinder`**, the most cognitively demanding flow here. When the way from here to the destination isn't visible yet, it charts a **shared map** of **decision tickets** on the issue tracker and resolves them one at a time — producing **decisions, not deliverables** — until the fog is pushed back and the way is clear. Where **`/grill-with-docs`** sharpens an idea you can hold in one session, wayfinder is for the idea you can't — and it's slower and denser, so save it for exactly that, never a well-scoped feature.
When the map clears, **it hands off, it doesn't build**: merge onto the main flow at **`/to-spec`**, which collapses the map's linked decisions into a buildable plan, then `/to-tickets` and `/implement` as usual. Looping the map straight into `/implement` skips that collapse and throws the linked detail away — go straight to `/implement` only when the effort turned out genuinely small.
## Codebase health
Not feature work — upkeep.
- **`/improve-codebase-architecture`** — run whenever you have a spare moment to keep the codebase good for agents to operate in. It surfaces **deepening opportunities**; picking one _generates an idea_ you can take into the main flow at `/grill-with-docs`. It's the survey that finds the candidates; **`/codebase-design`** (below) is the bench you design the chosen one on.
## Vocabulary underneath
Two model-invoked references that run *beneath* the other skills — each the single source of truth for its vocabulary. Reach for them directly when the **words**, not the process, are the problem; or let the skills above pull them in.
- **`/domain-modeling`** — sharpen the project's *domain* language: challenge a fuzzy term, resolve an overloaded word ("account" doing three jobs), record a hard-to-reverse decision as an ADR. It's the active discipline `/grill-with-docs` drives to keep `CONTEXT.md` a clean glossary.
- **`/codebase-design`** — the deep-module vocabulary (module, interface, depth, seam, adapter, leverage, locality) for designing a module's *shape*: a lot of behaviour behind a small interface at a clean seam. `/tdd` and `/improve-codebase-architecture` both speak it.
## Crossing sessions
- **`/handoff`** — when a thread is full or you need to branch off (e.g. into a `/prototype` session), this compacts the conversation into a markdown file. You don't continue in place — you **open a new session and reference that file** to carry the context across. It's the bridge between context windows, in either direction. Use it when you want a **fresh session** but need the **current conversation preserved**.
- **`/compact`** (built-in) — stay in the **same conversation**, letting the earlier turns be summarized. Use it at **intentional breaks between phases**, when you don't mind losing the verbatim history. Don't compact mid-phase — the agent can lose its way. `/handoff` forks; `/compact` continues.
## Standalone
Off the main flow entirely.
- **`/grill-me`** — the same relentless interview as `/grill-with-docs`, but for when you have **no codebase**. Stateless: it saves nothing locally, builds no `CONTEXT.md`. Reach for it to sharpen any plan or design that doesn't live in a repo.
- **`/prototype`** — a small, throwaway program that answers one design question: does this state model feel right, or what should this UI look like. Throwaway from day one — keep the answer, delete the code. It's the detour in step 2 of the main flow, but reach for it any time a design question is hard to settle on paper.
- **`/research`** — delegate reading legwork to a **background agent**: it investigates a question against **primary sources**, then leaves a cited Markdown file in the repo. Keep working while it reads. The file it produces is something to take *into* the main flow at `/grill-with-docs` — research feeds the thinking, it doesn't replace it.
- **`/teach`** — learn a concept over multiple sessions, using the current directory as a stateful workspace.
- **`/writing-great-skills`** — reference for writing and editing skills well.
## Precondition
**`/setup-matt-pocock-skills`** — run before your first engineering flow to configure the issue tracker, triage labels, and doc layout the other skills assume. Custom issue trackers also work.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Ask Matt"
short_description: "Find the right skill or workflow"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,49 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by dropping
filler, articles, and pleasantries while keeping full technical accuracy.
Use when user says "caveman mode", "talk like caveman", "use caveman",
"less tokens", "be brief", or invokes /caveman.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Persistence
ACTIVE EVERY RESPONSE once triggered. No revert after many turns. No filler drift. Still active if unsure. Off only when user says "stop caveman" or "normal mode".
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough.
Technical terms stay exact. Code blocks unchanged. Errors quoted exact.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
### Examples
**"Why React component re-render?"**
> Inline obj prop -> new ref -> re-render. `useMemo`.
**"Explain database connection pooling."**
> Pool = reuse DB conn. Skip handshake -> fast under load.
## Auto-Clarity Exception
Drop caveman temporarily for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.
Example -- destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
>
> ```sql
> DROP TABLE users;
> ```
>
> Caveman resume. Verify backup exist first.

View File

@@ -0,0 +1,89 @@
---
name: code-review
description: Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/PRD asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X".
---
Two-axis review of the diff between `HEAD` and a fixed point the user supplies:
- **Standards** — does the code conform to this repo's documented coding standards?
- **Spec** — does the code faithfully implement the originating issue / PRD / spec?
Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings.
The issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if `docs/agents/issue-tracker.md` is missing.
## Process
### 1. Pin the fixed point
Whatever the user said is the fixed point — a commit SHA, branch name, tag, `main`, `HEAD~5`, etc. If they didn't specify one, ask for it.
Capture the diff command once: `git diff <fixed-point>...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log <fixed-point>..HEAD --oneline`.
Before going further, confirm the fixed point resolves (`git rev-parse <fixed-point>`) and the diff is non-empty. A bad ref or empty diff should fail here — not inside two parallel sub-agents.
### 2. Identify the spec source
Look for the originating spec, in this order:
1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.) — fetch via the workflow in `docs/agents/issue-tracker.md`.
2. A path the user passed as an argument.
3. A PRD/spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature.
4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available".
### 3. Identify the standards sources
Anything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`.
On top of whatever the repo documents, the Standards axis always carries the **smell baseline** below — a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it:
- **The repo overrides.** A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell.
- **Always a judgement call.** Each smell is a labelled heuristic ("possible Feature Envy"), never a hard violation — and, like any standard here, skip anything tooling already enforces.
Each smell reads *what it is**how to fix*; match it against the diff:
- **Mysterious Name** — a function, variable, or type whose name doesn't reveal what it does or holds. → rename it; if no honest name comes, the design's murky.
- **Duplicated Code** — the same logic shape appears in more than one hunk or file in the change. → extract the shared shape, call it from both.
- **Feature Envy** — a method that reaches into another object's data more than its own. → move the method onto the data it envies.
- **Data Clumps** — the same few fields or params keep travelling together (a type wanting to be born). → bundle them into one type, pass that.
- **Primitive Obsession** — a primitive or string standing in for a domain concept that deserves its own type. → give the concept its own small type.
- **Repeated Switches** — the same `switch`/`if`-cascade on the same type recurs across the change. → replace with polymorphism, or one map both sites share.
- **Shotgun Surgery** — one logical change forces scattered edits across many files in the diff. → gather what changes together into one module.
- **Divergent Change** — one file or module is edited for several unrelated reasons. → split so each module changes for one reason.
- **Speculative Generality** — abstraction, parameters, or hooks added for needs the spec doesn't have. → delete it; inline back until a real need shows.
- **Message Chains** — long `a.b().c().d()` navigation the caller shouldn't depend on. → hide the walk behind one method on the first object.
- **Middle Man** — a class or function that mostly just delegates onward. → cut it, call the real target direct.
- **Refused Bequest** — a subclass or implementer that ignores or overrides most of what it inherits. → drop the inheritance, use composition.
### 4. Spawn both sub-agents in parallel
Send a single message with two `Agent` tool calls. Use the `general-purpose` subagent for both.
**Standards sub-agent prompt** — include:
- The full diff command and commit list.
- The list of standards-source files you found in step 3, **plus the smell baseline from step 3** pasted in full — the sub-agent has no other access to it.
- The brief: "Report — per file/hunk where relevant — (a) every place the diff violates a documented standard: cite the standard (file + the rule); and (b) any baseline smell you spot: name it and quote the hunk. Distinguish hard violations from judgement calls — documented-standard breaches can be hard, but baseline smells are always judgement calls, and a documented repo standard overrides the baseline. Skip anything tooling enforces. Under 400 words."
**Spec sub-agent prompt** — include:
- The diff command and commit list.
- The path or fetched contents of the spec.
- The brief: "Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words."
If the spec is missing, skip the Spec sub-agent and note this in the final report.
### 5. Aggregate
Present the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings — the two axes are deliberately separate (see _Why two axes_).
End with a one-line summary: total findings per axis, and the worst issue _within each axis_ (if any). Don't pick a single winner across axes — that's the reranking the separation exists to prevent.
## Why two axes
A change can pass one axis and fail the other:
- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.**
- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.**
Reporting them separately stops one axis from masking the other.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Code Review"
short_description: "Review a diff on standards and spec"

View File

@@ -0,0 +1,37 @@
# Deepening
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [SKILL.md](SKILL.md) — **module**, **interface**, **seam**, **adapter**.
## Dependency categories
When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam.
### 1. In-process
Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed.
### 2. Local-substitutable
Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface.
### 3. Remote but owned (Ports & Adapters)
Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter.
Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."*
### 4. True external (Mock)
Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter.
## Seam discipline
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection.
- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them.
## Testing strategy: replace, don't layer
- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them.
- Write new tests at the deepened module's interface. The **interface is the test surface**.
- Tests assert on observable outcomes through the interface, not internal state.
- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface.

View File

@@ -0,0 +1,44 @@
# Design It Twice
When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
Uses the vocabulary in [SKILL.md](SKILL.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
## Process
### 1. Frame the problem space
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
- The constraints any new interface would need to satisfy
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
### 2. Spawn sub-agents
Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module.
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
- Agent 1: "Minimize the interface — aim for 13 entry points max. Maximise leverage per entry point."
- Agent 2: "Maximise flexibility — support many use cases and extension."
- Agent 3: "Optimise for the most common caller — make the default case trivial."
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
Include both [SKILL.md](SKILL.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
Each sub-agent outputs:
1. Interface (types, methods, params — plus invariants, ordering, error modes)
2. Usage example showing how callers use it
3. What the implementation hides behind the seam
4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md))
5. Trade-offs — where leverage is high, where it's thin
### 3. Present and compare
Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**.
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu.

View File

@@ -0,0 +1,114 @@
---
name: codebase-design
description: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.
---
# Codebase Design
Design **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone.
## Glossary
Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.
**Module** — anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service.
**Interface** — everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow — they refer only to the type-level surface).
**Implementation** — what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
**Depth** — leverage at the interface: the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation.
**Seam** _(Michael Feathers)_ — a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context).
**Adapter** — a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside).
**Leverage** — what callers get from depth: more capability per unit of interface they learn. One implementation pays back across N call sites and M tests.
**Locality** — what maintainers get from depth: change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere.
## Deep vs shallow
**Deep module** = small interface + lots of implementation:
```
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
│ │
│ Deep Implementation│ ← Complex logic hidden
│ │
└─────────────────────┘
```
**Shallow module** = large interface + little implementation (avoid):
```
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
```
When designing an interface, ask:
- Can I reduce the number of methods?
- Can I simplify the parameters?
- Can I hide more complexity inside?
## Principles
- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.
- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape.
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.
## Designing for testability
Good interfaces make testing natural:
1. **Accept dependencies, don't create them.**
```typescript
// Testable
function processOrder(order, paymentGateway) {}
// Hard to test
function processOrder(order) {
const gateway = new StripeGateway();
}
```
2. **Return results, don't produce side effects.**
```typescript
// Testable
function calculateDiscount(cart): Discount {}
// Hard to test
function applyDiscount(cart): void {
cart.total -= discount;
}
```
3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup.
## Relationships
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
- **Depth** is a property of a **Module**, measured against its **Interface**.
- A **Seam** is where a **Module**'s **Interface** lives.
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
## Rejected framings
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.
- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**.
## Going deeper
- **Deepening a cluster given its dependencies** — see [DEEPENING.md](DEEPENING.md): dependency categories, seam discipline, and replace-don't-layer testing.
- **Exploring alternative interfaces** — see [DESIGN-IT-TWICE.md](DESIGN-IT-TWICE.md): spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Codebase Design"
short_description: "Vocabulary for deep-module design"

View File

@@ -0,0 +1,117 @@
---
name: diagnose
description: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression.
---
# Diagnose
A discipline for hard bugs. Skip phases only when explicitly justified.
When exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.
## Phase 1 — Build a feedback loop
**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.
Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**
### Ways to construct one — try them in roughly this order
1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.
2. **Curl / HTTP script** against a running dev server.
3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.
4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.
5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.
6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.
7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode.
8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it.
9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.
10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.
Build the right feedback loop, and the bug is 90% fixed.
### Iterate on the loop itself
Treat the loop as a product. Once you have _a_ loop, ask:
- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)
- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".)
- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)
A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.
### Non-deterministic bugs
The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.
### When you genuinely cannot build a loop
Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.
Do not proceed to Phase 2 until you have a loop you believe in.
## Phase 2 — Reproduce
Run the loop. Watch the bug appear.
Confirm:
- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.
- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).
- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.
Do not proceed until you reproduce the bug.
## Phase 3 — Hypothesise
Generate **35 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.
Each hypothesis must be **falsifiable**: state the prediction it makes.
> Format: "If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse."
If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.
**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.
## Phase 4 — Instrument
Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.**
Tool preference:
1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.
2. **Targeted logs** at the boundaries that distinguish hypotheses.
3. Never "log everything and grep".
**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.
**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.
## Phase 5 — Fix + regression test
Write the regression test **before the fix** — but only if there is a **correct seam** for it.
A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.
**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.
If a correct seam exists:
1. Turn the minimised repro into a failing test at that seam.
2. Watch it fail.
3. Apply the fix.
4. Watch it pass.
5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.
## Phase 6 — Cleanup + post-mortem
Required before declaring done:
- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)
- [ ] Regression test passes (or absence of seam is documented)
- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)
- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)
- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns
**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Human-in-the-loop reproduction loop.
# Copy this file, edit the steps below, and run it.
# The agent runs the script; the user follows prompts in their terminal.
#
# Usage:
# bash hitl-loop.template.sh
#
# Two helpers:
# step "<instruction>" → show instruction, wait for Enter
# capture VAR "<question>" → show question, read response into VAR
#
# At the end, captured values are printed as KEY=VALUE for the agent to parse.
set -euo pipefail
step() {
printf '\n>>> %s\n' "$1"
read -r -p " [Enter when done] " _
}
capture() {
local var="$1" question="$2" answer
printf '\n>>> %s\n' "$question"
read -r -p " > " answer
printf -v "$var" '%s' "$answer"
}
# --- edit below ---------------------------------------------------------
step "Open the app at http://localhost:3000 and sign in."
capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)"
capture ERROR_MSG "Paste the error message (or 'none'):"
# --- edit above ---------------------------------------------------------
printf '\n--- Captured ---\n'
printf 'ERRORED=%s\n' "$ERRORED"
printf 'ERROR_MSG=%s\n' "$ERROR_MSG"

View File

@@ -0,0 +1,134 @@
---
name: diagnosing-bugs
description: Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
---
# Diagnosing Bugs
A discipline for hard bugs. Skip phases only when explicitly justified.
When exploring the codebase, read `CONTEXT.md` (if it exists) to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.
## Phase 1 — Build a feedback loop
**This is the skill.** Everything else is mechanical. If you have a **tight** pass/fail signal for the bug — one that goes red on _this_ bug — you will find the cause; bisection, hypothesis-testing, and instrumentation all just consume it. If you don't have one, no amount of staring at code will save you.
Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**
### Ways to construct one — try them in roughly this order
1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.
2. **Curl / HTTP script** against a running dev server.
3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.
4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.
5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.
6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.
7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode.
8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it.
9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.
10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.
Build the right feedback loop, and the bug is 90% fixed.
### Tighten the loop
Treat the loop as a product. Once you have _a_ loop, **tighten** it:
- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)
- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".)
- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)
A 30-second flaky loop is barely better than no loop; a 2-second deterministic one is tight — a debugging superpower.
### Non-deterministic bugs
The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.
### When you genuinely cannot build a loop
Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.
### Completion criterion — a tight loop that goes red
Phase 1 is done when the loop is **tight** and **red-capable**: you can name **one command** — a script path, a test invocation, a curl — that you have **already run at least once** (paste the invocation and its output), and that is:
- [ ] **Red-capable** — it drives the actual bug code path and asserts the **user's exact symptom**, so it can go red on this bug and green once fixed. Not "runs without erroring" — it must be able to _catch this specific bug_.
- [ ] **Deterministic** — same verdict every run (flaky bugs: a pinned, high reproduction rate, per above).
- [ ] **Fast** — seconds, not minutes.
- [ ] **Agent-runnable** — you can run it unattended; a human in the loop only via `scripts/hitl-loop.template.sh`.
If you catch yourself reading code to build a theory before this command exists, **stop — jumping straight to a hypothesis is the exact failure this skill prevents.** No red-capable command, no Phase 2.
## Phase 2 — Reproduce + minimise
Run the loop. Watch it go red — the bug appears.
Confirm:
- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.
- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).
- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.
### Minimise
Once it's red, shrink the repro to the **smallest scenario that still goes red**. Cut inputs, callers, config, data, and steps **one at a time**, re-running the loop after each cut — keep only what's load-bearing for the failure.
Why bother: a minimal repro shrinks the hypothesis space in Phase 3 (fewer moving parts left to suspect) and becomes the clean regression test in Phase 5.
Done when **every remaining element is load-bearing** — removing any one of them makes the loop go green.
Do not proceed until you have reproduced **and** minimised.
## Phase 3 — Hypothesise
Generate **35 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.
Each hypothesis must be **falsifiable**: state the prediction it makes.
> Format: "If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse."
If you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.
**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.
## Phase 4 — Instrument
Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.**
Tool preference:
1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.
2. **Targeted logs** at the boundaries that distinguish hypotheses.
3. Never "log everything and grep".
**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.
**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.
## Phase 5 — Fix + regression test
Write the regression test **before the fix** — but only if there is a **correct seam** for it.
A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.
**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.
If a correct seam exists:
1. Turn the minimised repro into a failing test at that seam.
2. Watch it fail.
3. Apply the fix.
4. Watch it pass.
5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.
## Phase 6 — Cleanup + post-mortem
Required before declaring done:
- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)
- [ ] Regression test passes (or absence of seam is documented)
- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)
- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)
- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns
**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Diagnosing Bugs"
short_description: "Diagnose hard bugs and regressions"

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Human-in-the-loop reproduction loop.
# Copy this file, edit the steps below, and run it.
# The agent runs the script; the user follows prompts in their terminal.
#
# Usage:
# bash hitl-loop.template.sh
#
# Two helpers:
# step "<instruction>" → show instruction, wait for Enter
# capture VAR "<question>" → show question, read response into VAR
#
# At the end, captured values are printed as KEY=VALUE for the agent to parse.
set -euo pipefail
step() {
printf '\n>>> %s\n' "$1"
read -r -p " [Enter when done] " _
}
capture() {
local var="$1" question="$2" answer
printf '\n>>> %s\n' "$question"
read -r -p " > " answer
printf -v "$var" '%s' "$answer"
}
# --- edit below ---------------------------------------------------------
step "Open the app at http://localhost:3000 and sign in."
capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)"
capture ERROR_MSG "Paste the error message (or 'none'):"
# --- edit above ---------------------------------------------------------
printf '\n--- Captured ---\n'
printf 'ERRORED=%s\n' "$ERRORED"
printf 'ERROR_MSG=%s\n' "$ERROR_MSG"

View File

@@ -0,0 +1,47 @@
# ADR Format
ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
Create the `docs/adr/` directory lazily — only when the first ADR is needed.
## Template
```md
# {Short title of the decision}
{1-3 sentences: what's the context, what did we decide, and why.}
```
That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.
## Optional sections
Only include these when they add genuine value. Most ADRs won't need them.
- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
- **Considered Options** — only when the rejected alternatives are worth remembering
- **Consequences** — only when non-obvious downstream effects need to be called out
## Numbering
Scan `docs/adr/` for the highest existing number and increment by one.
## When to offer an ADR
All three of these must be true:
1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
### What qualifies
- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.

View File

@@ -0,0 +1,60 @@
# CONTEXT.md Format
## Structure
```md
# {Context Name}
{One or two sentence description of what this context is and why it exists.}
## Language
**Order**:
{A one or two sentence description of the term}
_Avoid_: Purchase, transaction
**Invoice**:
A request for payment sent to a customer after delivery.
_Avoid_: Bill, payment request
**Customer**:
A person or organization that places orders.
_Avoid_: Client, buyer, account
```
## Rules
- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`.
- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does.
- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
## Single vs multi-context repos
**Single context (most repos):** One `CONTEXT.md` at the repo root.
**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
```md
# Context Map
## Contexts
- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
## Relationships
- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
```
The skill infers which structure applies:
- If `CONTEXT-MAP.md` exists, read it to find contexts
- If only a root `CONTEXT.md` exists, single context
- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.

View File

@@ -0,0 +1,74 @@
---
name: domain-modeling
description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.
---
# Domain Modeling
Actively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.)
## File structure
Most repos have a single context:
```
/
├── CONTEXT.md
├── docs/
│ └── adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
```
/
├── CONTEXT-MAP.md
├── docs/
│ └── adr/ ← system-wide decisions
├── src/
│ ├── ordering/
│ │ ├── CONTEXT.md
│ │ └── docs/adr/ ← context-specific decisions
│ └── billing/
│ ├── CONTEXT.md
│ └── docs/adr/
```
Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
## During the session
### Challenge against the glossary
When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"
### Sharpen fuzzy language
When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."
### Discuss concrete scenarios
When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
### Cross-reference with code
When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"
### Update CONTEXT.md inline
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.
### Offer ADRs sparingly
Only offer to create an ADR when all three are true:
1. **Hard to reverse** — the cost of changing your mind later is meaningful
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Domain Modeling"
short_description: "Build and sharpen a domain model"

View File

@@ -0,0 +1,7 @@
---
name: grill-me
description: A relentless interview to sharpen a plan or design.
disable-model-invocation: true
---
Run a `/grilling` session.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Grill Me"
short_description: "Sharpen a plan through interview"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,7 @@
---
name: grill-with-docs
description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
disable-model-invocation: true
---
Run a `/grilling` session, using the `/domain-modeling` skill.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Grill with Docs"
short_description: "Grill a design and write its docs"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,12 @@
---
name: grilling
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
---
Interview me relentlessly about every aspect of this until we reach a shared understanding. Walk down each branch of the decision tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering.
If a *fact* can be found by exploring the environment (filesystem, tools, etc.), look it up rather than asking me. The *decisions*, though, are mine — put each one to me and wait for my answer.
Do not act on it until I confirm we have reached a shared understanding.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Grilling"
short_description: "Stress-test thinking one question at a time"

View File

@@ -0,0 +1,16 @@
---
name: handoff
description: Compact the current conversation into a handoff document for another agent to pick up.
argument-hint: "What will the next session be used for?"
disable-model-invocation: true
---
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the temporary directory of the user's OS - not the current workspace.
Include a "suggested skills" section in the document, which suggests skills that the agent should invoke.
Do not duplicate content already captured in other artifacts (specs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead.
Redact any sensitive information, such as API keys, passwords, or personally identifiable information.
If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Handoff"
short_description: "Compact a conversation into a handoff"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,15 @@
---
name: implement
description: "Implement a piece of work based on a spec or set of tickets."
disable-model-invocation: true
---
Implement the work described by the user in the spec or tickets.
Use /tdd where possible, at pre-agreed seams.
Run typechecking regularly, single test files regularly, and the full test suite once at the end.
Once done, use /code-review to review the work.
Commit your work to the current branch.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Implement"
short_description: "Build work from a spec or tickets"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,123 @@
# HTML Report Format
The architectural review is rendered as a single self-contained HTML file in the OS temp directory. Tailwind and Mermaid both come from CDNs. Mermaid handles graph-shaped diagrams reliably; hand-built divs and inline SVG handle the more editorial visuals (mass diagrams, cross-sections). Mix the two — don't lean on Mermaid for everything, it'll start to look generic.
## Scaffold
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Architecture review — {{repo name}}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
mermaid.initialize({ startOnLoad: true, theme: "neutral", securityLevel: "loose" });
</script>
<style>
/* small custom layer for things Tailwind doesn't cover cleanly:
dashed seam lines, hand-drawn-feeling arrow heads, etc. */
.seam { stroke-dasharray: 4 4; }
.leak { stroke: #dc2626; }
.deep { background: linear-gradient(135deg, #0f172a, #1e293b); }
</style>
</head>
<body class="bg-stone-50 text-slate-900 font-sans">
<main class="max-w-5xl mx-auto px-6 py-12 space-y-12">
<header>...</header>
<section id="candidates" class="space-y-10">...</section>
<section id="top-recommendation">...</section>
</main>
</body>
</html>
```
## Header
Repo name, date, and a compact legend: solid box = module, dashed line = seam, red arrow = leakage, thick dark box = deep module. No introduction paragraph — straight into the candidates.
## Candidate card
The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms (from the `/codebase-design` skill) without ceremony.
Each candidate is one `<article>`:
- **Title** — short, names the deepening (e.g. "Collapse the Order intake pipeline").
- **Badge row** — recommendation strength (`Strong` = emerald, `Worth exploring` = amber, `Speculative` = slate), plus a tag for the dependency category (`in-process`, `local-substitutable`, `ports & adapters`, `mock`).
- **Files** — monospaced list, `font-mono text-sm`.
- **Before / After diagram** — the centrepiece. Two columns, side by side. See patterns below.
- **Problem** — one sentence. What hurts.
- **Solution** — one sentence. What changes.
- **Wins** — bullets, ≤6 words each. e.g. "Tests hit one interface", "Pricing logic stops leaking", "Delete 4 shallow wrappers".
- **ADR callout** (if applicable) — one line in an amber-tinted box.
No paragraphs of explanation. If the diagram needs a paragraph to be understood, redraw the diagram.
## Diagram patterns
Pick the pattern that fits the candidate. Mix them. Don't make every diagram look the same — variety is part of the point.
### Mermaid graph (the workhorse for dependencies / call flow)
Use a Mermaid `flowchart` or `graph` when the point is "X calls Y calls Z, and look at the mess." Wrap it in a Tailwind-styled card so it doesn't feel parachuted in. Style with classDef to colour leakage edges red and the deep module dark. Sequence diagrams work well for "before: 6 round-trips; after: 1."
```html
<div class="rounded-lg border border-slate-200 bg-white p-4">
<pre class="mermaid">
flowchart LR
A[OrderHandler] --> B[OrderValidator]
B --> C[OrderRepo]
C -.leak.-> D[PricingClient]
classDef leak stroke:#dc2626,stroke-width:2px;
class C,D leak
</pre>
</div>
```
### Hand-built boxes-and-arrows (when Mermaid's layout fights you)
Modules as `<div>`s with borders and labels. Arrows as inline SVG `<line>` or `<path>` elements positioned absolutely over a relative container. Reach for this when you want the "after" diagram to feel like one thick-bordered deep module with greyed-out internals — Mermaid won't render that with the right weight.
### Cross-section (good for layered shallowness)
Stack horizontal bands (`h-12 border-l-4`) to show layers a call passes through. Before: 6 thin layers each doing nothing. After: 1 thick band labelled with the consolidated responsibility.
### Mass diagram (good for "interface as wide as implementation")
Two rectangles per module — one for interface surface area, one for implementation. Before: interface rectangle is nearly as tall as the implementation rectangle (shallow). After: interface rectangle is short, implementation rectangle is tall (deep).
### Call-graph collapse
Before: a tree of function calls rendered as nested boxes. After: the same tree collapsed into one box, with the now-internal calls shown faded inside it.
## Style guidance
- Lean editorial, not corporate-dashboard. Generous whitespace. Serif optional for headings (`font-serif` works well with stone/slate).
- Colour sparingly: one accent (emerald or indigo) plus red for leakage and amber for warnings.
- Keep diagrams ~320px tall so before/after sits comfortably side by side without scrolling.
- Use `text-xs uppercase tracking-wider` for module labels inside diagrams — they should read as schematic, not as UI.
- The only scripts are the Tailwind CDN and the Mermaid ESM import. The report is otherwise static — no app code, no interactivity beyond Mermaid's own rendering.
## Top recommendation section
One larger card. Candidate name, one sentence on why, anchor link to its card. That's it.
## Tone
Plain English, concise — but the architectural nouns and verbs come straight from the `/codebase-design` skill. Concision is not an excuse to drift.
**Use exactly:** module, interface, implementation, depth, deep, shallow, seam, adapter, leverage, locality.
**Never substitute:** component, service, unit (for module) · API, signature (for interface) · boundary (for seam) · layer, wrapper (for module, when you mean module).
**Phrasings that fit the style:**
- "Order intake module is shallow — interface nearly matches the implementation."
- "Pricing leaks across the seam."
- "Deepen: one interface, one place to test."
- "Two adapters justify the seam: HTTP in prod, in-memory in tests."
**Wins bullets** name the gain in glossary terms: *"locality: bugs concentrate in one module"*, *"leverage: one interface, N call sites"*, *"interface shrinks; implementation absorbs the wrappers"*. Don't write *"easier to maintain"* or *"cleaner code"* — those terms aren't in the glossary and don't earn their place.
No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in the `/codebase-design` glossary, reach for one that is before inventing a new one.

View File

@@ -0,0 +1,71 @@
---
name: improve-codebase-architecture
description: Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.
disable-model-invocation: true
---
# Improve Codebase Architecture
Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.
This command is _informed_ by the project's domain model and built on a shared design vocabulary:
- Run the `/codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, "the interface is the test surface", "one adapter = hypothetical seam, two = real"). Use these terms exactly in every suggestion — don't drift into "component," "service," "API," or "boundary."
- The domain language in `CONTEXT.md` gives names to good seams; ADRs in `docs/adr/` record decisions this command should not re-litigate.
## Process
### 1. Explore
**Scope before you scan — YAGNI.** Deepening a module pays off by making future changes to it easier, so put extra weight on the parts of the codebase that have recently changed. Decide *where* to look before you look:
- If the user named a direction — a module, a subsystem, a pain point — take it, and skip the inference below.
- Otherwise, walk back a good stretch of the commit history (`git log --oneline`) to find the codebase's hot spots — the files and areas that keep coming up — and let those paths pull your attention first. If the changes are scattered with no clear hot spot, widen the net.
Read the project's domain glossary (`CONTEXT.md`) and any ADRs in the area you're touching first.
Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:
- Where does understanding one concept require bouncing between many small modules?
- Where are modules **shallow** — interface nearly as complex as the implementation?
- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?
- Where do tightly-coupled modules leak across their seams?
- Which parts of the codebase are untested, or hard to test through their current interface?
Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want.
### 2. Present candidates as an HTML report
Write a self-contained HTML file to the OS temp directory so nothing lands in the repo. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `<tmpdir>/architecture-review-<timestamp>.html` so each run gets a fresh file. Open it for the user — `xdg-open <path>` on Linux, `open <path>` on macOS, `start <path>` on Windows — and tell them the absolute path.
The report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals — use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual.
For each candidate, render a card with:
- **Files** — which files/modules are involved
- **Problem** — why the current architecture is causing friction
- **Solution** — plain English description of what would change
- **Benefits** — explained in terms of locality and leverage, and how tests would improve
- **Before / After diagram** — side-by-side, custom-drawn, illustrating the shallowness and the deepening
- **Recommendation strength** — one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge
End the report with a **Top recommendation** section: which candidate you'd tackle first and why.
**Use CONTEXT.md vocabulary for the domain, and the `/codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the card (e.g. a warning callout: _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
See [HTML-REPORT.md](HTML-REPORT.md) for the full HTML scaffold, diagram patterns, and styling guidance.
Do NOT propose interfaces yet. After the file is written, ask the user: "Which of these would you like to explore?"
### 3. Grilling loop
Once the user picks a candidate, run the `/grilling` skill to walk the decision tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
Side effects happen inline as decisions crystallize — run the `/domain-modeling` skill to keep the domain model current as you go:
- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md`. Create the file lazily if it doesn't exist.
- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones.
- **Want to explore alternative interfaces for the deepened module?** Run the `/codebase-design` skill and use its design-it-twice parallel sub-agent pattern.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Improve Codebase Architecture"
short_description: "Find and grill architecture improvements"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,79 @@
# Logic Prototype
A tiny interactive terminal app that lets the user drive a state model by hand. Use this when the question is about **business logic, state transitions, or data shape** — the kind of thing that looks reasonable on paper but only feels wrong once you push it through real cases.
## When this is the right shape
- "I'm not sure if this state machine handles the edge case where X then Y."
- "Does this data model actually let me represent the case where..."
- "I want to feel out what the API should look like before writing it."
- Anything where the user wants to **press buttons and watch state change**.
If the question is "what should this look like" — wrong branch. Use [UI.md](UI.md).
## Process
### 1. State the question
Before writing code, write down what state model and what question you're prototyping. One paragraph, in the prototype's README or a comment at the top of the file. A logic prototype that answers the wrong question is pure waste — make the question explicit so it can be checked later, whether the user is watching now or returning to it AFK.
### 2. Pick the language
Use whatever the host project uses. If the project has no obvious runtime (e.g. a docs repo), ask.
Match the project's existing conventions for tooling — don't add a new package manager or runtime just for the prototype.
### 3. Isolate the logic in a portable module
Put the actual logic — the bit that's answering the question — behind a small, pure interface that could be lifted out and dropped into the real codebase later. The TUI around it is throwaway; the logic module shouldn't be.
The right shape depends on the question:
- **A pure reducer** — `(state, action) => state`. Good when actions are discrete events and state is a single value.
- **A state machine** — explicit states and transitions. Good when "which actions are even legal right now" is part of the question.
- **A small set of pure functions** over a plain data type. Good when there's no implicit current state — just transformations.
- **A class or module with a clear method surface** when the logic genuinely owns ongoing internal state.
Pick whichever shape best fits the question being asked, *not* whichever is easiest to wire to a TUI. Keep it pure: no I/O, no terminal code, no `console.log` for control flow. The TUI imports it and calls into it; nothing flows the other direction.
This is what makes the prototype useful past its own lifetime: when the question's been answered, the validated reducer / machine / function set can be lifted into the real module on its own.
### 4. Build the smallest TUI that exposes the state
Build it as a **lightweight TUI** — on every tick, clear the screen (`console.clear()` / `print("\033[2J\033[H")` / equivalent) and re-render the whole frame. The user should always see one stable view, not an ever-growing scrollback.
Each frame has two parts, in this order:
1. **Current state**, pretty-printed and diff-friendly (one field per line, or formatted JSON). Use **bold** for field names or section headers and **dim** for less important context (timestamps, IDs, derived values). Native ANSI escape codes are fine — `\x1b[1m` bold, `\x1b[2m` dim, `\x1b[0m` reset. No need to pull in a styling library unless one is already in the project.
2. **Keyboard shortcuts**, listed at the bottom: `[a] add user [d] delete user [t] tick clock [q] quit`. Bold the key, dim the description, or vice-versa — whatever reads cleanly.
Behaviour:
1. **Initialise state** — a single in-memory object/struct. Render the first frame on start.
2. **Read one keystroke (or one line)** at a time, dispatch to a handler that mutates state.
3. **Re-render** the full frame after every action — don't append, replace.
4. **Loop until quit.**
The whole frame should fit on one screen.
### 5. Make it runnable in one command
Add a script to the project's existing task runner (`package.json` scripts, `Makefile`, `justfile`, `pyproject.toml`). The user should run `pnpm run <prototype-name>` or equivalent — never need to remember a path.
If the host project has no task runner, just put the command at the top of the prototype's README.
### 6. Hand it over
Give the user the run command. They'll drive it themselves; the interesting moments are when they say "wait, that shouldn't be possible" or "huh, I assumed X would be different" — those are the bugs in the _idea_, which is the whole point. If they want new actions added, add them. Prototypes evolve.
### 7. Capture the answer and the prototype
Once the prototype has answered its question, capture the answer, then capture the prototype the way the [SKILL](SKILL.md) describes. The logic-specific mapping: the validated reducer / machine / function set lifts into the real module (the decision, absorbed); the TUI shell rides along to the throwaway branch that keeps the prototype as a primary source.
## Anti-patterns
- **Don't add tests.** A prototype that needs tests is no longer a prototype.
- **Don't wire it to the real database.** Use an in-memory store unless the question is specifically about persistence.
- **Don't generalise.** No "what if we wanted to support X later." The prototype answers one question.
- **Don't blur the logic and the TUI together.** If the reducer / state machine references `console.log`, prompts, or terminal escape codes, it's no longer portable. Keep the TUI as a thin shell over a pure module.
- **Don't ship the TUI shell into production.** The shell is optimised for being driven by hand from a terminal. The logic module behind it is the bit worth keeping.

View File

@@ -0,0 +1,26 @@
---
name: prototype
description: Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
---
# Prototype
A prototype is **throwaway code that answers a question**. The question decides the shape.
## Pick a branch
Identify which question is being answered — from the user's prompt, the surrounding code, or by asking if the user is around:
- **"Does this logic / state model feel right?"** → [LOGIC.md](LOGIC.md). Build a tiny interactive terminal app that pushes the state machine through cases that are hard to reason about on paper.
- **"What should this look like?"** → [UI.md](UI.md). Generate several radically different UI variations on a single route, switchable via a URL search param and a floating bottom bar.
The two branches produce very different artifacts — getting this wrong wastes the whole prototype. If the question is genuinely ambiguous and the user isn't reachable, default to whichever branch better matches the surrounding code (a backend module → logic; a page or component → UI) and state the assumption at the top of the prototype.
## Rules that apply to both
1. **Throwaway from day one, and clearly marked as such.** Locate the prototype code close to where it will actually be used (next to the module or page it's prototyping for) so context is obvious — but name it so a casual reader can see it's a prototype, not production. For throwaway UI routes, obey whatever routing convention the project already uses; don't invent a new top-level structure.
2. **One command to run.** Whatever the project's existing task runner supports — `pnpm <name>`, `python <path>`, `bun <path>`, etc. The user must be able to start it without thinking.
3. **No persistence by default.** State lives in memory. Persistence is the thing the prototype is _checking_, not something it should depend on. If the question explicitly involves a database, hit a scratch DB or a local file with a clear "PROTOTYPE — wipe me" name.
4. **Skip the polish.** No tests, no error handling beyond what makes the prototype _runnable_, no abstractions. The point is to learn something fast.
5. **Surface the state.** After every action (logic) or on every variant switch (UI), print or render the full relevant state so the user can see what changed.
6. **Capture it when done.** Fold any validated decision into the real code, then capture the prototype itself as a **primary source**: commit it to a throwaway branch, out of main, and leave a context pointer to that branch on the implementation issue. Capture the answer too — the verdict and the question it settled — in the issue or a commit. The main branch keeps only the validated decision.

View File

@@ -0,0 +1,112 @@
# UI Prototype
Generate **several radically different UI variations** on a single route, switchable from a floating bottom bar. The user flips between variants in the browser, picks one (or steals bits from each), then throws the rest away.
If the question is about logic/state rather than what something looks like — wrong branch. Use [LOGIC.md](LOGIC.md).
## When this is the right shape
- "What should this page look like?"
- "I want to see a few options for this dashboard before committing."
- "Try a different layout for the settings screen."
- Any time the user would otherwise spend a day picking between three vague mockups in their head.
## Two sub-shapes — strongly prefer sub-shape A
A UI prototype is much easier to judge when it's **butting up against the rest of the app** — real header, real sidebar, real data, real density. A throwaway route on its own is a vacuum: every variant looks fine in isolation. Default to sub-shape A whenever there's a plausible existing page to host the variants. Only reach for sub-shape B if the prototype genuinely has no nearby home.
### Sub-shape A — adjustment to an existing page (preferred)
The route already exists. Variants are rendered **on the same route**, gated by a `?variant=` URL search param. The existing data fetching, params, and auth all stay — only the rendering swaps. This is the default; pick it unless there's a specific reason not to.
If the prototype is for something that doesn't yet have a page but *would naturally live inside one* (a new section of the dashboard, a new card on the settings screen, a new step in an existing flow) — that's still sub-shape A. Mount the variants inside the host page.
### Sub-shape B — a new page (last resort)
Only use this when the thing being prototyped genuinely has no existing page to live inside — e.g. an entirely new top-level surface, or a flow that can't be embedded anywhere sensible.
Create a **throwaway route** following whatever routing convention the project already uses — don't invent a new top-level structure. Name it so it's obviously a prototype (e.g. include the word `prototype` in the path or filename). Same `?variant=` pattern.
Before committing to sub-shape B, sanity-check: is there really no existing page this could be embedded in? An empty route hides design problems that a populated one would expose.
In both sub-shapes the floating bottom bar is identical.
## Process
### 1. State the question and pick N
Default to **3 variants**. More than 5 stops being radically different and starts being noise — cap there.
Write down the plan in one line, in the prototype's location or a top-of-file comment:
> "Three variants of the settings page, switchable via `?variant=`, on the existing `/settings` route."
This works whether the user is here to push back or not.
### 2. Generate radically different variants
Draft each variant. Hold each one to:
- The page's purpose and the data it has access to.
- The project's component library / styling system (TailwindCSS, shadcn, MUI, plain CSS, whatever).
- A clear exported component name, e.g. `VariantA`, `VariantB`, `VariantC`.
Variants must be **structurally different** — different layout, different information hierarchy, different primary affordance, not just different colours. Three slightly-tweaked card grids isn't a UI prototype, it's wallpaper. If two drafts come out too similar, redo one with explicit "do not use a card grid" guidance.
### 3. Wire them together
Create a single switcher component on the route:
```tsx
// pseudo-code — adapt to the project's framework
const variant = searchParams.get('variant') ?? 'A';
return (
<>
{variant === 'A' && <VariantA {...data} />}
{variant === 'B' && <VariantB {...data} />}
{variant === 'C' && <VariantC {...data} />}
<PrototypeSwitcher variants={['A','B','C']} current={variant} />
</>
);
```
For sub-shape A (existing page): keep all the existing data fetching above the switcher; only the rendered subtree changes per variant.
For sub-shape B (new page): the throwaway route under `/prototype/<name>` mounts the same switcher.
### 4. Build the floating switcher
A small fixed-position bar at the bottom-centre of the screen with three pieces:
- **Left arrow** — cycles to the previous variant (wraps around).
- **Variant label** — shows the current variant key and, if the variant exports a name, that name too. e.g. `B — Sidebar layout`.
- **Right arrow** — cycles forward (wraps around).
Behaviour:
- Clicking an arrow updates the URL search param (use the framework's router — `router.replace` on Next, `navigate` on React Router, etc) so the variant is shareable and reload-stable.
- Keyboard: `←` and `→` arrow keys also cycle. Don't intercept arrow keys when an `<input>`, `<textarea>`, or `[contenteditable]` is focused.
- Visually distinct from the page (e.g. high-contrast pill, subtle shadow) so it's obviously not part of the design being evaluated.
- Hidden in production builds — gate on `process.env.NODE_ENV !== 'production'` or an equivalent check, so a stray prototype merge can't ship the bar to users.
Put the switcher in a single shared component so both sub-shapes can reuse it. Locate it wherever shared UI lives in the project.
### 5. Hand it over
Surface the URL (and the `?variant=` keys). The user will flip through whenever they get to it. The interesting feedback is usually **"I want the header from B with the sidebar from C"** — that's the actual design they want.
### 6. Capture the answer and clean up
Once a variant has won, capture the answer — which variant and why — then capture the prototype the way the [SKILL](SKILL.md) describes. Fold the winner into the real code and move the rest onto the throwaway branch, not into main:
- **Sub-shape A** — fold the winner into the existing page; drop the losing variants and the switcher from main.
- **Sub-shape B** — promote the winning variant to a real route; drop the throwaway route and the switcher from main.
The full set of variants is the primary source, so it lands on the throwaway branch, not the bin — variant components and the switcher left in the main branch rot fast and confuse the next reader.
## Anti-patterns
- **Variants that differ only in colour or copy.** That's a tweak, not a prototype. Real variants disagree about structure.
- **Sharing too much code between variants.** A shared `<Header>` is fine; a shared `<Layout>` defeats the point. Each variant should be free to throw out the layout.
- **Wiring variants to real mutations.** Read-only prototypes are fine. If a variant needs to mutate, point it at a stub — the question is "what should this look like", not "does the backend work".
- **Promoting the prototype directly to production.** The variant code was written under prototype constraints (no tests, minimal error handling). Rewrite it properly when you fold it in.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Prototype"
short_description: "Prototype to answer a design question"

View File

@@ -0,0 +1,12 @@
---
name: research
description: Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.
---
Spin up a **background agent** to do the research, so you keep working while it reads.
Its job:
1. Investigate the question against **primary sources** — official docs, source code, specs, first-party APIs — not a secondary write-up of them. Follow every claim back to the source that owns it.
2. Write the findings to a single Markdown file, citing each claim's source.
3. Save it where the repo already keeps such notes; match the existing convention, and if there is none, put it somewhere sensible and say where.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Research"
short_description: "Research from high-trust sources"

View File

@@ -0,0 +1,14 @@
---
name: resolving-merge-conflicts
description: "Use when you need to resolve an in-progress git merge/rebase conflict."
---
1. **See the current state** of the merge/rebase. Check git history, and the conflicting files.
2. **Find the primary sources** for each conflict. Understand deeply why each change was made, and what the original intent was. Read the commit messages, check the PRs, check original issues/tickets.
3. **Resolve each hunk.** Preserve both intents where possible. Where incompatible, pick the one matching the merge's stated goal and note the trade-off. Do **not** invent new behaviour. Always resolve; never `--abort`.
4. Discover the project's **automated checks** and run them — typically typecheck, then tests, then format. Fix anything the merge broke.
5. **Finish the merge/rebase.** Stage everything and commit. If rebasing, continue the rebase process until all commits are rebased.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "Resolving Merge Conflicts"
short_description: "Resolve merge and rebase conflicts"

View File

@@ -0,0 +1,116 @@
---
name: setup-matt-pocock-skills
description: Configure this repo for the engineering skills — set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
disable-model-invocation: true
---
# Setup Matt Pocock's Skills
Scaffold the per-repo configuration that the engineering skills assume:
- **Issue tracker** — where issues live (GitHub by default; local markdown is also supported out of the box)
- **Triage labels** — the strings used for the five canonical triage roles
- **Domain docs** — where `CONTEXT.md` and ADRs live, and the consumer rules for reading them
This is a prompt-driven skill, not a deterministic script. Explore, present what you found, confirm with the user, then write.
## Process
### 1. Explore
Look at the current repo to understand its starting state. Read whatever exists; don't assume:
- `git remote -v` and `.git/config` — is this a GitHub repo? Which one?
- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either?
- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root
- `docs/adr/` and any `src/*/docs/adr/` directories
- `docs/agents/` — does this skill's prior output already exist?
- `.scratch/` — sign that a local-markdown issue tracker convention is already in use
- Is the `triage` skill installed? (a `triage` skill folder alongside this one, or `triage` in your available skills.) This decides whether Section B runs at all.
- Monorepo signals — a `pnpm-workspace.yaml`, a `workspaces` field in `package.json`, or a populated `packages/*` with its own `src/`. Present only in a genuinely large multi-package repo; their absence means single-context, which is almost every repo.
### 2. Present findings and ask
Summarise what's present and what's missing. Then take the sections in order — one section, one answer, then the next.
Lead each section with the recommended answer so the user can accept it in a word. Give a one-line explainer only when the choice genuinely branches; skip the section entirely when exploration already settled it (Section B when `triage` isn't installed, Section C when there's no monorepo).
**Section A — Issue tracker.**
> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-tickets`, `triage`, `to-spec`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo.
Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. Otherwise (or if the user prefers), offer:
- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI)
- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI)
- **Local markdown** — issues live as files under `.scratch/<feature>/` in this repo (good for solo projects or repos without a remote)
- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose
Record the choice in `docs/agents/issue-tracker.md`. The GitHub and GitLab templates carry a "PRs as a request surface" flag, defaulted **off** — leave it off and don't raise it; a user who wants external PRs in the triage queue can flip the flag in the file later.
**Section B — Triage label vocabulary.** Skip this section entirely if the `triage` skill isn't installed (exploration told you) — an uninstalled skill needs no labels.
If it is installed, ask exactly one question:
> Do you want to keep the default triage labels? (recommended: **yes**)
The defaults are the five canonical roles, each label string equal to its name: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. On **yes**, write them as-is. Only if the user says no — usually because their tracker already uses other names (e.g. `bug:triage` for `needs-triage`) — collect the overrides so `triage` applies existing labels instead of creating duplicates.
**Section C — Domain docs.** Default to **single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. This fits almost every repo; write it without asking.
Offer **multi-context** — a root `CONTEXT-MAP.md` pointing to per-context `CONTEXT.md` files — only when exploration found monorepo signals. Then confirm which layout they want.
### 3. Confirm and edit
Show the user a draft of:
- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules)
- The contents of `docs/agents/issue-tracker.md`, `docs/agents/domain.md`, and `docs/agents/triage-labels.md` (the last only when `triage` is installed)
Let them edit before writing.
### 4. Write
**Pick the file to edit:**
- If `CLAUDE.md` exists, edit it.
- Else if `AGENTS.md` exists, edit it.
- If neither exists, ask the user which one to create — don't pick for them.
Never create `AGENTS.md` when `CLAUDE.md` already exists (or vice versa) — always edit the one that's already there.
If an `## Agent skills` block already exists in the chosen file, update its contents in-place rather than appending a duplicate. Don't overwrite user edits to the surrounding sections.
The block:
```markdown
## Agent skills
### Issue tracker
[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`.
### Triage labels
[one-line summary of the label vocabulary]. See `docs/agents/triage-labels.md`.
### Domain docs
[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`.
```
Include the `### Triage labels` sub-block, and write `docs/agents/triage-labels.md`, only when `triage` is installed and Section B ran. When it isn't, both are omitted.
Then write the docs files using the seed templates in this skill folder as a starting point:
- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker
- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker
- [issue-tracker-local.md](./issue-tracker-local.md) — local-markdown issue tracker
- [triage-labels.md](./triage-labels.md) — label mapping (only if `triage` is installed)
- [domain.md](./domain.md) — domain doc consumer rules + layout
For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description.
### 5. Done
Tell the user the setup is complete and which engineering skills will now read from these files. Mention they can edit `docs/agents/*.md` directly later — re-running this skill is only necessary if they want to switch issue trackers or restart from scratch.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Setup Matt Pocock Skills"
short_description: "Configure a repo for the skills"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,51 @@
# Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Before exploring, read these
- **`CONTEXT.md`** at the repo root, or
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
## File structure
Single-context repo (most repos):
```
/
├── CONTEXT.md
├── docs/adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
```
/
├── CONTEXT-MAP.md
├── docs/adr/ ← system-wide decisions
└── src/
├── ordering/
│ ├── CONTEXT.md
│ └── docs/adr/ ← context-specific decisions
└── billing/
├── CONTEXT.md
└── docs/adr/
```
## Use the glossary's vocabulary
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_

View File

@@ -0,0 +1,45 @@
# Issue tracker: GitHub
Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
## Conventions
- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
- **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
- **Comment on an issue**: `gh issue comment <number> --body "..."`
- **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
- **Close**: `gh issue close <number> --comment "..."`
Infer the repo from `git remote -v``gh` does this automatically when run inside a clone.
## Pull requests as a triage surface
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
- **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`).
- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`.
## When a skill says "publish to the issue tracker"
Create a GitHub issue.
## When a skill says "fetch the relevant ticket"
Run `gh issue view <number> --comments`.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
- **Claim**: `gh issue edit <n> --add-assignee @me` — the session's first write.
- **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.

View File

@@ -0,0 +1,46 @@
# Issue tracker: GitLab
Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations.
## Conventions
- **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor.
- **Read an issue**: `glab issue view <number> --comments`. Use `-F json` for machine-readable output.
- **List issues**: `glab issue list -F json` with appropriate `--label` filters.
- **Comment on an issue**: `glab issue note <number> --message "..."`. GitLab calls comments "notes".
- **Apply / remove labels**: `glab issue update <number> --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag.
- **Close**: `glab issue close <number>`. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note <number> --message "..."`, then close.
- **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`.
Infer the repo from `git remote -v``glab` does this automatically when run inside a clone.
## Merge requests as a triage surface
**MRs as a request surface: no.** _(Set to `yes` if this repo treats external merge requests as feature requests; `/triage` reads this flag.)_
When set to `yes`, MRs run through the same labels and states as issues, using the `glab mr` equivalents:
- **Read an MR**: `glab mr view <number> --comments` and `glab mr diff <number>` for the diff.
- **List external MRs for triage**: `glab mr list -F json`, then keep only MRs whose author is not a project member/owner (a contributor's MR, not a maintainer's in-flight work).
- **Comment / label / close**: `glab mr note`, `glab mr update --label`/`--unlabel`, `glab mr close`.
Unlike GitHub, GitLab numbers issues and MRs separately, so `#42` is unambiguous once you know which surface the maintainer means.
## When a skill says "publish to the issue tracker"
Create a GitLab issue.
## When a skill says "fetch the relevant ticket"
Run `glab issue view <number> --comments`.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `glab issue create --label wayfinder:map`. (On GitLab tiers with native epics, an epic may hold the map instead; a labelled issue works everywhere.)
- **Child ticket**: an issue carrying `Part of #<map>` at the top of its description and labels `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
- **Blocking**: GitLab's **native blocking link** — the canonical, UI-visible representation. Add it with the `/blocked_by #<n>` quick action, posted as a note (`glab issue note <child> --message "/blocked_by #<blocker>"`). Native blocking links are a Premium/Ultimate feature; on the free tier (or where unavailable) fall back to a `Blocked by: #<n>, #<n>` line at the top of the description. A ticket is unblocked when every blocker is closed.
- **Frontier query**: `glab issue list -F json` scoped to the map's children, drop any with an open blocker — a native `blocked_by` link to an open issue (`glab api projects/:id/issues/:iid/links`), or an open issue in the `Blocked by` line — or an assignee; first in map order wins.
- **Claim**: `glab issue update <n> --assignee @me` — the session's first write.
- **Resolve**: `glab issue note <n> --message "<answer>"`, then `glab issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.

View File

@@ -0,0 +1,30 @@
# Issue tracker: Local Markdown
Issues and specs (you may know a spec as a PRD) for this repo live as markdown files in `.scratch/`.
## Conventions
- One feature per directory: `.scratch/<feature-slug>/`
- The spec is `.scratch/<feature-slug>/spec.md`
- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` — never a single combined tickets file
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
## When a skill says "publish to the issue tracker"
Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
## When a skill says "fetch the relevant ticket"
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
## Wayfinding operations
Used by `/wayfinder`. The **map** is a file with one **child** file per ticket.
- **Map**: `.scratch/<effort>/map.md` — the Notes / Decisions-so-far / Fog body.
- **Child ticket**: `.scratch/<effort>/issues/NN-<slug>.md`, numbered from `01`, with the question in the body. A `Type:` line records the ticket type (`research`/`prototype`/`grilling`/`task`); a `Status:` line records `claimed`/`resolved`.
- **Blocking**: a `Blocked by: NN, NN` line near the top. A ticket is unblocked when every file it lists is `resolved`.
- **Frontier**: scan `.scratch/<effort>/issues/` for files that are open, unblocked, and unclaimed; first by number wins.
- **Claim**: set `Status: claimed` and save before any work.
- **Resolve**: append the answer under an `## Answer` heading, set `Status: resolved`, then append a context pointer (gist + link) to the map's Decisions-so-far in `map.md`.

View File

@@ -0,0 +1,15 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
Edit the right-hand column to match whatever vocabulary you actually use.

View File

@@ -0,0 +1,36 @@
---
name: tdd
description: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
---
# Test-Driven Development
TDD is the red → green loop. This skill is the reference that makes that loop produce tests worth keeping: what a good test is, where tests go, the anti-patterns, and the rules of the loop. Every section applies on every cycle — consult them before and during the loop, not after.
When exploring the codebase, read `CONTEXT.md` (if it exists) so test names and interface vocabulary match the project's domain language, and respect ADRs in the area you're touching.
## What a good test is
Tests verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't. A good test reads like a specification — "user can checkout with valid cart" tells you exactly what capability exists — and survives refactors because it doesn't care about internal structure.
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
## Seams — where tests go
A **seam** is the public boundary you test at: the interface where you observe behavior without reaching inside. Tests live at seams, never against internals.
**Test only at pre-agreed seams.** Before writing any test, write down the seams under test and confirm them with the user. No test is written at an unconfirmed seam. You can't test everything — agreeing the seams up front is how testing effort lands on the critical paths and complex logic instead of every edge case.
Ask: "What's the public interface, and which seams should we test?"
## Anti-patterns
- **Implementation-coupled** — mocks internal collaborators, tests private methods, or verifies through a side channel (querying the database instead of using the interface). The tell: the test breaks when you refactor but behavior hasn't changed.
- **Tautological** — the assertion recomputes the expected value the way the code does (`expect(add(a, b)).toBe(a + b)`, a snapshot derived by hand the same way, a constant asserted equal to itself), so it passes by construction and can never disagree with the code. Expected values must come from an independent source of truth — a known-good literal, a worked example, the spec.
- **Horizontal slicing** — writing all tests first, then all implementation. Bulk tests verify _imagined_ behavior: you test the _shape_ of things rather than user-facing behavior, the tests go insensitive to real changes, and you commit to test structure before understanding the implementation. Work in **vertical slices** instead — one test → one implementation → repeat, each test a **tracer bullet** that responds to what the last cycle taught you.
## Rules of the loop
- **Red before green.** Write the failing test first, then only enough code to pass it. Don't anticipate future tests or add speculative features.
- **One slice at a time.** One seam, one test, one minimal implementation per cycle.
- **Refactoring is not part of the loop.** It belongs to the review stage (see the `code-review` skill), not the red → green implementation cycle.

View File

@@ -0,0 +1,3 @@
interface:
display_name: "TDD"
short_description: "Test-driven red-green-refactor"

View File

@@ -0,0 +1,59 @@
# When to Mock
Mock at **system boundaries** only:
- External APIs (payment, email, etc.)
- Databases (sometimes - prefer test DB)
- Time/randomness
- File system (sometimes)
Don't mock:
- Your own classes/modules
- Internal collaborators
- Anything you control
## Designing for Mockability
At system boundaries, design interfaces that are easy to mock:
**1. Use dependency injection**
Pass external dependencies in rather than creating them internally:
```typescript
// Easy to mock
function processPayment(order, paymentClient) {
return paymentClient.charge(order.total);
}
// Hard to mock
function processPayment(order) {
const client = new StripeClient(process.env.STRIPE_KEY);
return client.charge(order.total);
}
```
**2. Prefer SDK-style interfaces over generic fetchers**
Create specific functions for each external operation instead of one generic function with conditional logic:
```typescript
// GOOD: Each function is independently mockable
const api = {
getUser: (id) => fetch(`/users/${id}`),
getOrders: (userId) => fetch(`/users/${userId}/orders`),
createOrder: (data) => fetch('/orders', { method: 'POST', body: data }),
};
// BAD: Mocking requires conditional logic inside the mock
const api = {
fetch: (endpoint, options) => fetch(endpoint, options),
};
```
The SDK approach means:
- Each mock returns one specific shape
- No conditional logic in test setup
- Easier to see which endpoints a test exercises
- Type safety per endpoint

View File

@@ -0,0 +1,77 @@
# Good and Bad Tests
## Good Tests
**Integration-style**: Test through real interfaces, not mocks of internal parts.
```typescript
// GOOD: Tests observable behavior
test("user can checkout with valid cart", async () => {
const cart = createCart();
cart.add(product);
const result = await checkout(cart, paymentMethod);
expect(result.status).toBe("confirmed");
});
```
Characteristics:
- Tests behavior users/callers care about
- Uses public API only
- Survives internal refactors
- Describes WHAT, not HOW
- One logical assertion per test
## Bad Tests
**Implementation-detail tests**: Coupled to internal structure.
```typescript
// BAD: Tests implementation details
test("checkout calls paymentService.process", async () => {
const mockPayment = jest.mock(paymentService);
await checkout(cart, payment);
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});
```
Red flags:
- Mocking internal collaborators
- Testing private methods
- Asserting on call counts/order
- Test breaks when refactoring without behavior change
- Test name describes HOW not WHAT
- Verifying through external means instead of interface
```typescript
// BAD: Bypasses interface to verify
test("createUser saves to database", async () => {
await createUser({ name: "Alice" });
const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);
expect(row).toBeDefined();
});
// GOOD: Verifies through interface
test("createUser makes user retrievable", async () => {
const user = await createUser({ name: "Alice" });
const retrieved = await getUser(user.id);
expect(retrieved.name).toBe("Alice");
});
```
**Tautological tests**: Expected value restates the implementation, so the test passes by construction.
```typescript
// BAD: Expected value is recomputed the way the code computes it
test("calculateTotal sums line items", () => {
const items = [{ price: 10 }, { price: 5 }];
const expected = items.reduce((sum, i) => sum + i.price, 0);
expect(calculateTotal(items)).toBe(expected);
});
// GOOD: Expected value is an independent, known literal
test("calculateTotal sums line items", () => {
expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15);
});
```

View File

@@ -0,0 +1,35 @@
# GLOSSARY.md Format
`GLOSSARY.md` is the canonical language for this teaching workspace. All explainers, exercises, and learning records should adhere to its terminology. Building it is itself part of learning: compressing a concept into a tight definition is evidence the user understands it.
## Structure
```md
# {Topic} Glossary
{One or two sentence description of the topic this glossary covers.}
## Terms
**Hypertrophy**:
Muscle growth driven by mechanical tension and metabolic stress over repeated training sessions.
_Avoid_: Bulking, getting big
**Progressive overload**:
Systematically increasing the demand on a muscle over time — via load, volume, or intensity.
_Avoid_: Pushing harder, levelling up
**RPE (Rate of Perceived Exertion)**:
A 110 self-rating of how hard a set felt, where 10 is failure and 8 means two reps left in the tank.
_Avoid_: Effort score, intensity rating
```
## Rules
- **Add a term only when the user understands it.** The glossary is a record of compressed knowledge, not a dictionary the user reads to learn. If the user has just been introduced to a concept, wait until they can use it correctly before promoting it here.
- **Be opinionated.** When several words exist for the same concept, pick the best one and list the rest as aliases to avoid. This is how language compresses.
- **Keep definitions tight.** One or two sentences. Define what the term IS, not what it does or how to do it.
- **Use the glossary's own terms inside definitions.** Once a term is in the glossary, prefer it everywhere — including inside other definitions. This is what makes complex terms easier to grasp later.
- **Group under subheadings** when natural clusters emerge (e.g. `## Anatomy`, `## Programming`). A flat list is fine when terms cohere.
- **Flag ambiguities explicitly.** If a term is used loosely in the wider field, note the resolution: "In this workspace, 'set' always means a working set — warm-ups are tracked separately."
- **Revise as understanding deepens.** A definition the user wrote in week one may be wrong by week six. Update in place; do not leave stale entries.

View File

@@ -0,0 +1,46 @@
# Learning Record Format
Learning records live in `./learning-records/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. Create the directory lazily — only when the first record is written.
They are the teaching equivalent of ADRs: they capture non-obvious lessons, key insights, and stated prior knowledge that will steer future sessions. They are used to calculate the zone of proximal development.
## Template
```md
# {Short title of what was learned or established}
{1-3 sentences: what was learned (or what prior knowledge was established), and why it matters for future sessions.}
```
That is the whole format. A learning record can be a single paragraph. The value is recording _that_ this is now known and _why_ it changes what to teach next — not in filling out sections.
## Optional sections
Only include these when they add genuine value. Most records won't need them.
- **Status** frontmatter (`active | superseded by LR-NNNN`) — useful when an earlier understanding turns out to be wrong and is replaced.
- **Evidence** — how the user demonstrated the understanding (a question answered, an exercise completed, prior experience cited). Useful when the claim might be revisited.
- **Implications** — what this unlocks or rules out for future sessions. Worth recording when non-obvious.
## Numbering
Scan `./learning-records/` for the highest existing number and increment by one.
## When to write a learning record
Write one when any of these is true:
1. **The user demonstrated genuine understanding of something non-trivial** — not just exposure, but evidence they can use the concept correctly. This sets a new floor for what to teach next.
2. **The user disclosed prior knowledge** — "I already know X." Record it so future sessions don't re-teach it. Also record the _depth_ claimed.
3. **A misconception was corrected** — the user previously believed something wrong and now sees why. These are high-value: they predict future stumbling blocks for related topics.
4. **The mission shifted in response to learning** — the user discovered they cared about something different than they thought. Cross-link to [[MISSION.md]] and update it.
### What does _not_ qualify
- Material that was merely covered. Coverage is not learning. Wait for evidence.
- Anything already captured tersely in [[GLOSSARY.md]] as a term definition. Don't duplicate.
- Session-by-session activity logs. Learning records are not a journal — they are decision-grade insights.
## Supersession
When a later record contradicts an earlier one (the user's understanding deepened or corrected), mark the old record `Status: superseded by LR-NNNN` rather than deleting it. The history of how understanding evolved is itself useful signal.

View File

@@ -0,0 +1,31 @@
# MISSION.md Format
`MISSION.md` lives at the workspace root. It captures the _reason_ the user is learning this topic. Every teaching decision — what to teach next, which resources to surface, which exercises to design — should trace back to this document.
## Template
```md
# Mission: {Topic}
## Why
{1-3 sentences. The concrete real-world goal the user is chasing. What changes in their life or work when they have this skill? Avoid abstract framings like "to understand X" — push for the underlying outcome.}
## Success looks like
- {A specific, observable thing the user will be able to do}
- {Another specific thing}
- {…}
## Constraints
- {Time, budget, prior commitments, learning preferences, anything that bounds the approach}
## Out of scope
- {Adjacent topics the user explicitly does not want to chase right now — protects the zone of proximal development}
```
## Rules
- **One mission per workspace.** If the user wants to learn two unrelated things, that is two workspaces.
- **Concrete over abstract.** "Run a half marathon by October" beats "get fitter." "Ship a Rust CLI to my team" beats "learn Rust."
- **Push back on vagueness.** If the user cannot articulate why, interview them before writing anything. A bad mission is worse than no mission.
- **Revise when reality shifts.** Missions change. When the user's goal moves, update this file — don't leave a stale mission steering future sessions.
- **Keep it short.** If `MISSION.md` runs past a screen, it has stopped being a compass and started being a plan.

View File

@@ -0,0 +1,32 @@
# RESOURCES.md Format
`RESOURCES.md` is the curated set of trusted sources for this topic. Knowledge for explainers should be drawn from here, not from parametric guesses. Wisdom comes from the communities listed here.
## Structure
```md
# {Topic} Resources
## Knowledge
- [Book: _The Science and Practice of Strength Training_ — Zatsiorsky & Kraemer](https://example.com)
Foundational text on programming and adaptation. Use for: anything to do with periodisation, recovery, intensity zones.
- [Article: "How Much Should I Train?" — Greg Nuckols (Stronger By Science)](https://example.com)
Evidence-based review of volume landmarks. Use for: weekly set targets per muscle group.
## Wisdom (Communities)
- [r/weightroom](https://reddit.com/r/weightroom)
High-signal subreddit, moderated against bro-science. Use for: programme critique, plateau troubleshooting.
- Local: Tuesday strength class at {gym name}
Use for: real-time coaching feedback on lifts.
```
## Rules
- **High-trust only.** Prefer primary sources, recognised experts, peer-reviewed work, and communities with strong moderation. If a resource is marketing dressed as education, leave it out.
- **Annotate every entry.** A bare link is useless in three months. Add one line: what it covers and when to reach for it.
- **Group by Knowledge / Wisdom.** Mirrors the philosophy in [SKILL.md](./SKILL.md). It is fine for a resource to appear in only one group.
- **Surface gaps explicitly.** If no good resource exists for an area the mission needs, write a `## Gaps` section listing what is missing. This drives future search.
- **Prune ruthlessly.** A resource that turned out to be wrong, shallow, or off-mission should be removed, not buried. Better five sharp sources than thirty mediocre ones.
- **Record community preferences.** If the user has opted out of joining communities, note it here so future sessions don't keep proposing them.

View File

@@ -0,0 +1,140 @@
---
name: teach
description: Teach the user a new skill or concept, within this workspace.
disable-model-invocation: true
argument-hint: "What would you like to learn about?"
---
The user has asked you to teach them something. This is a stateful request - they intend to learn the topic over multiple sessions.
## Teaching Workspace
Treat the current directory as a teaching workspace. The state of their learning is captured in this directory in several files:
- `MISSION.md`: A document capturing the _reason_ the user is interested in the topic. This should be used to ground all teaching. Use the format in [MISSION-FORMAT.md](./MISSION-FORMAT.md).
- `./reference/*.html`: A directory of reference materials. These are the compressed learnings from the lessons - cheat sheets, reference algorithms, syntax, yoga poses, glossaries. They are the raw units of learning. They should be beautiful documents which print out well, and are designed for quick reference.
- `RESOURCES.md`: A list of resources which can be explored to ground your teaching in contextual knowledge, or to acquire knowledge and wisdom. Use the format in [RESOURCES-FORMAT.md](./RESOURCES-FORMAT.md).
- `./learning-records/*.md`: A directory of learning records, which capture what the user has learned. These are loosely equivalent to architectural decision records in software development - they capture non-obvious lessons and key insights that may need to be revised later, or drive future sessions. These should be used to calculate the zone of proximal development. They are titled `0001-<dash-case-name>.md`, where the number increments each time. Use the format in [LEARNING-RECORD-FORMAT.md](./LEARNING-RECORD-FORMAT.md).
- `./lessons/*.html`: A directory of lessons. A **lesson** is a single, self-contained HTML output that teaches one tightly-scoped thing tied to the mission. This is the primary unit of teaching in this workspace.
- `./assets/*`: Reusable **components** shared across lessons. See [Assets](#assets).
- `NOTES.md`: A scratchpad for you to jot down user preferences, or working notes.
## Philosophy
To learn at a deep level, the user needs three things:
- **Knowledge**, captured from high-quality, high-trust resources
- **Skills**, acquired through highly-relevant interactive lessons devised by you, based on the knowledge
- **Wisdom**, which comes from interacting with other learners and practitioners
Before the `RESOURCES.md` is well-populated, your focus should be to find high-quality resources which will help the user acquire knowledge. Never trust your parametric knowledge.
Some topics may require more skills than knowledge. Learning more about theoretical physics might be more knowledge-based. For yoga, more skills-based.
### Fluency vs Storage Strength
You should be careful to split between two types of learning:
- **Fluency strength**: in-the-moment retrieval of knowledge
- **Storage strength**: long-term retention of knowledge
Fluency can give the user an illusory sense of mastery, but storage strength is the real goal. Try to design lessons which build long-term retention by desirable difficulty:
- Using retrieval practice (recall from memory)
- Spacing (distributing practice over time)
- Interleaving (mixing up different but related topics in practice - for skills practice only)
## Lessons
A lesson is the main thing you produce — the unit in which knowledge and skills reach the user. Each lesson is one self-contained HTML file, saved to `./lessons/` and titled `0001-<dash-case-name>.html` where the number increments each time.
A lesson should be **beautiful** — clean, readable typography and layout — since the user will return to these later to review. Think Tufte.
The lesson should be short, and completable very quickly. Learners' working memory is very small, and we need to stay within it. But each lesson should give the user a single tangible win that they can build on. It should be directly tied to the mission, and should be in the user's zone of proximal development.
If possible, open the lesson file for the user by running a CLI command.
Each lesson should link via HTML anchors to other lessons and reference documents.
Each lesson should recommend a primary source for the user to read or watch. This should be the most high-quality, high-trust resource you found on the topic.
Each lesson should contain a reminder to ask followup questions to the agent. The agent is their teacher, and can assist with anything that's unclear.
## Assets
Lessons are built from reusable **components**, stored in `./assets/`: stylesheets, quiz widgets, simulators, diagram helpers — anything a second lesson could reuse.
Reuse is the default, not the exception. Before authoring a lesson, read `./assets/` and build from the components already there. When a lesson needs something new and reusable, write it as a component in `./assets/` and link to it — never inline code a future lesson would duplicate.
A shared stylesheet is the first component every workspace earns: every lesson links it, so the lessons look like one consistent course rather than a pile of one-offs. As the workspace grows, so should the component library.
## The Mission
Every lesson should be tied into the mission - the reason that the user is interested in learning about the topic.
If the user is unclear about the mission, or the `MISSION.md` is not populated, your first job should be to question the user on why they want to learn this.
Failing to understand the mission will mean knowledge acquisition is not grounded in real-world goals. Lessons will feel too abstract. You will have no way of judging what the user should do next.
Missions may change as the user develops more skills and knowledge. This is normal - make sure to update the `MISSION.md` and add a learning record to capture the change. Confirm with the user before changing the mission.
## Zone Of Proximal Development
Each lesson, the user should always feel as if they are being challenged 'just enough'.
The user may specify an exact thing they want to learn. If they don't, figure out their zone of proximal development by:
- Reading their `learning-records`
- Figuring out the right thing to teach them based on their mission
- Teach the most relevant thing that fits in their zone of proximal development
## Knowledge
Lessons should be designed around a skill the user is going to learn. The knowledge in the lesson should be only what's required to acquire that skill. You teach the knowledge first, then get the user to practice the skills via an interactive feedback loop.
Knowledge should first be gathered from trusted resources. Use `RESOURCES.md` to keep track of them. Lessons should be littered with citations - links to external resources to back up any claim made. This increases the trustworthiness of the lesson.
For acquiring knowledge, difficulty is the enemy. It eats working memory you need for understanding.
## Skills
If knowledge is all about acquisition, skills are about durability and flexibility. Make the knowledge stick.
For skill acquisition, difficulty is the tool. Effortful retrieval is what builds storage strength. Skills should be taught through interactive lessons. There are several tools at your disposal:
- Interactive lessons, using quizzes and light in-browser tasks
- Lessons which guide the user through a list of real-world steps to take (for instance, yoga poses)
Each of these should be based on a **feedback loop**, where the user receives feedback on their performance. This feedback loop should be as tight as possible, giving feedback immediately - and ideally automatically.
For quizzes, each answer should be exactly the same number of words (and characters, if possible). Don't give the user any clues about the answer through formatting.
## Acquiring Wisdom
Wisdom comes from true real-world interaction - testing your skills outside the learning environment.
When the user asks a question that appears to require wisdom, your default posture should be to attempt to answer - but to ultimately delegate to a **community**.
A community is a place (online or offline) where the user can test their skills in the real world. This might be a forum, a subreddit, a real-world class (budget permitting) or a local interest group.
You should attempt to find high-reputation communities the user can join. If the user expresses a preference that they don't want to join a community, respect it.
## Reference Documents
While creating lessons, you should also create reference documents. Lessons can reference these documents - they are useful for tracking raw units of knowledge useful across lessons.
Lessons will rarely be revisited later - reference documents will be. They should be the compressed essence of the lesson, in a format designed for quick reference.
Some learning topics lend themselves to reference:
- Syntax and code snippets for programming
- Algorithms and flowcharts for processes
- Yoga poses and sequences for yoga
- Exercises and routines for fitness
- Glossaries for any topic with its own nomenclature
Glossaries, in particular, are an essential reference. Once one is created, it should be adhered to in every lesson.
## `NOTES.md`
The user will sometimes express preferences of how they want to be taught, or things you should keep in mind. This is the place to record those preferences, so you can refer back to them when designing lessons or working with the user.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Teach"
short_description: "Learn a concept in a guided workspace"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,84 @@
---
name: to-issues
description: Break a plan, spec, or PRD into independently-grabbable issues on the project issue tracker using tracer-bullet vertical slices.
disable-model-invocation: true
---
# To Issues
Break a plan into independently-grabbable issues using vertical slices (tracer bullets).
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
## Process
### 1. Gather context
Work from whatever is already in the conversation context. If the user passes an issue reference (issue number, URL, or path) as an argument, fetch it from the issue tracker and read its full body and comments.
### 2. Explore the codebase (optional)
If you have not already explored the codebase, do so to understand the current state of the code. Issue titles and descriptions should use the project's domain glossary vocabulary, and respect ADRs in the area you're touching.
Look for opportunities to prefactor the code to make the implementation easier. "Make the change easy, then make the easy change."
### 3. Draft vertical slices
Break the plan into **tracer bullet** issues. Each issue is a thin vertical slice that cuts through ALL integration layers end-to-end, NOT a horizontal slice of one layer.
<vertical-slice-rules>
- Each slice delivers a narrow but COMPLETE path through every layer (schema, API, UI, tests)
- A completed slice is demoable or verifiable on its own
- Any prefactoring should be done first
</vertical-slice-rules>
### 4. Quiz the user
Present the proposed breakdown as a numbered list. For each slice, show:
- **Title**: short descriptive name
- **Blocked by**: which other slices (if any) must complete first
- **User stories covered**: which user stories this addresses (if the source material has them)
Ask the user:
- Does the granularity feel right? (too coarse / too fine)
- Are the dependency relationships correct?
- Should any slices be merged or split further?
Iterate until the user approves the breakdown.
### 5. Publish the issues to the issue tracker
For each approved slice, publish a new issue to the issue tracker. Use the issue body template below. These issues are considered ready for AFK agents, so publish them with the correct triage label unless instructed otherwise.
Publish issues in dependency order (blockers first) so you can reference real issue identifiers in the "Blocked by" field.
<issue-template>
## Parent
A reference to the parent issue on the issue tracker (if the source was an existing issue, otherwise omit this section).
## What to build
A concise description of this vertical slice. Describe the end-to-end behavior, not layer-by-layer implementation.
Avoid specific file paths or code snippets — they go stale fast. Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it here and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
## Acceptance criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Criterion 3
## Blocked by
- A reference to the blocking ticket (if any)
Or "None - can start immediately" if no blockers.
</issue-template>
Do NOT close or modify any parent issue.

View File

@@ -0,0 +1,75 @@
---
name: to-prd
description: Turn the current conversation into a PRD and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed.
disable-model-invocation: true
---
This skill takes the current conversation context and codebase understanding and produces a PRD. Do NOT interview the user — just synthesize what you already know.
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
## Process
1. Explore the repo to understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the PRD, and respect any ADRs in the area you're touching.
2. Sketch out the seams at which you're going to test the feature. Existing seams should be preferred to new ones. Use the highest seam possible. If new seams are needed, propose them at the highest point you can. The fewer seams across the codebase, the better - the ideal number is one.
Check with the user that these seams match their expectations.
3. Write the PRD using the template below, then publish it to the project issue tracker. Apply the `ready-for-agent` triage label - no need for additional triage.
<prd-template>
## Problem Statement
The problem that the user is facing, from the user's perspective.
## Solution
The solution to the problem, from the user's perspective.
## User Stories
A LONG, numbered list of user stories. Each user story should be in the format of:
1. As an <actor>, I want a <feature>, so that <benefit>
<user-story-example>
1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending
</user-story-example>
This list of user stories should be extremely extensive and cover all aspects of the feature.
## Implementation Decisions
A list of implementation decisions that were made. This can include:
- The modules that will be built/modified
- The interfaces of those modules that will be modified
- Technical clarifications from the developer
- Architectural decisions
- Schema changes
- API contracts
- Specific interactions
Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.
Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it within the relevant decision and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
## Testing Decisions
A list of testing decisions that were made. Include:
- A description of what makes a good test (only test external behavior, not implementation details)
- Which modules will be tested
- Prior art for the tests (i.e. similar types of tests in the codebase)
## Out of Scope
A description of the things that are out of scope for this PRD.
## Further Notes
Any further notes about the feature.
</prd-template>

View File

@@ -0,0 +1,75 @@
---
name: to-spec
description: Turn the current conversation into a spec and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed.
disable-model-invocation: true
---
This skill takes the current conversation context and codebase understanding and produces a spec (you may know this document as a PRD). Do NOT interview the user — just synthesize what you already know.
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
## Process
1. Explore the repo to understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the spec, and respect any ADRs in the area you're touching.
2. Sketch out the seams at which you're going to test the feature. Existing seams should be preferred to new ones. Use the highest seam possible. If new seams are needed, propose them at the highest point you can. The fewer seams across the codebase, the better - the ideal number is one.
Check with the user that these seams match their expectations.
3. Write the spec using the template below, then publish it to the project issue tracker. Apply the `ready-for-agent` triage label - no need for additional triage.
<spec-template>
## Problem Statement
The problem that the user is facing, from the user's perspective.
## Solution
The solution to the problem, from the user's perspective.
## User Stories
A LONG, numbered list of user stories. Each user story should be in the format of:
1. As an <actor>, I want a <feature>, so that <benefit>
<user-story-example>
1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending
</user-story-example>
This list of user stories should be extremely extensive and cover all aspects of the feature.
## Implementation Decisions
A list of implementation decisions that were made. This can include:
- The modules that will be built/modified
- The interfaces of those modules that will be modified
- Technical clarifications from the developer
- Architectural decisions
- Schema changes
- API contracts
- Specific interactions
Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.
Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it within the relevant decision and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
## Testing Decisions
A list of testing decisions that were made. Include:
- A description of what makes a good test (only test external behavior, not implementation details)
- Which modules will be tested
- Prior art for the tests (i.e. similar types of tests in the codebase)
## Out of Scope
A description of the things that are out of scope for this spec.
## Further Notes
Any further notes about the feature.
</spec-template>

View File

@@ -0,0 +1,5 @@
interface:
display_name: "To Spec"
short_description: "Turn a conversation into a spec"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,107 @@
---
name: to-tickets
description: Break a plan, spec, or the current conversation into a set of tracer-bullet tickets, each declaring its blocking edges, published to the configured tracker — edges as text in one file per ticket locally, or native blocking links on a real tracker.
disable-model-invocation: true
---
# To Tickets
Break a plan, spec, or conversation into a set of **tickets** — tracer-bullet vertical slices, each declaring the tickets that **block** it.
The issue tracker and triage label vocabulary should have been provided to you — run `/setup-matt-pocock-skills` if not.
## Process
### 1. Gather context
Work from whatever is already in the conversation context. If the user passes a reference (a spec path, an issue number or URL) as an argument, fetch it and read its full body and comments.
### 2. Explore the codebase (optional)
If you have not already explored the codebase, do so to understand the current state of the code. Ticket titles and descriptions should use the project's domain glossary vocabulary, and respect ADRs in the area you're touching.
Look for opportunities to prefactor the code to make the implementation easier. "Make the change easy, then make the easy change."
### 3. Draft vertical slices
Break the work into **tracer bullet** tickets.
<vertical-slice-rules>
- Each slice cuts a narrow but COMPLETE path through every layer (schema, API, UI, tests) — vertical, NOT a horizontal slice of one layer
- A completed slice is demoable or verifiable on its own
- Each slice is sized to fit in a single fresh context window
- Any prefactoring should be done first
</vertical-slice-rules>
Give each ticket its **blocking edges** — the other tickets that must complete before it can start. A ticket with no blockers can start immediately.
**Wide refactors are the exception to vertical slicing.** A **wide refactor** is one mechanical change — rename a column, retype a shared symbol — whose **blast radius** fans across the whole codebase, so a single edit breaks thousands of call sites at once and no vertical slice can land green. Don't force it into a tracer bullet; sequence it as **expandcontract**. First expand: add the new form beside the old so nothing breaks. Then migrate the call sites over in batches sized by blast radius (per package, per directory), each batch its own ticket blocked by the expand, keeping CI green batch to batch because the old form still exists. Finally contract: delete the old form once no caller remains, in a ticket blocked by every migrate batch. When even the batches can't stay green alone, keep the sequence but let them share an integration branch that all block a final integrate-and-verify ticket — green is promised only there.
### 4. Quiz the user
Present the proposed breakdown as a numbered list. For each ticket, show:
- **Title**: short descriptive name
- **Blocked by**: which other tickets (if any) must complete first
- **What it delivers**: the end-to-end behaviour this ticket makes work
Ask the user:
- Does the granularity feel right? (too coarse / too fine)
- Are the blocking edges correct — does each ticket only depend on tickets that genuinely gate it?
- Should any tickets be merged or split further?
Iterate until the user approves the breakdown.
### 5. Publish the tickets to the configured tracker
Publish the approved tickets. **How** depends on the tracker `/setup-matt-pocock-skills` configured — the tickets are the same either way, only the shape of the blocking edges changes:
- **Local files** → write one file per ticket under `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` in dependency order (blockers first). Each file's "Blocked by" lists the numbers/titles it depends on. Use the per-ticket file template below — one ticket per file, never a single combined file.
- **A real issue tracker (GitHub, Linear, …)** → publish one issue per ticket in dependency order (blockers first) so each ticket's blocking edges can reference real identifiers. Use the platform's native blocking / sub-issue relationship where it has one; otherwise set each ticket's "Blocked by" to the blocking issues. Apply the `ready-for-agent` triage label unless instructed otherwise — the tickets are agent-grabbable by construction.
Work the **frontier**: any ticket whose blockers are all done. For a purely linear chain that means top to bottom.
Do NOT close or modify any parent issue.
<local-ticket-template>
# <NN> — <Ticket title>
**What to build:** the end-to-end behaviour this ticket makes work, from the user's perspective — not a layer-by-layer implementation list.
**Blocked by:** the numbers/titles of the tickets that gate this one, or "None — can start immediately".
**Status:** ready-for-agent
- [ ] Acceptance criterion 1
- [ ] Acceptance criterion 2
</local-ticket-template>
<issue-template>
## Parent
A reference to the parent issue on the tracker (if the source was an existing issue, otherwise omit this section).
## What to build
The end-to-end behaviour this ticket makes work, from the user's perspective — not layer-by-layer implementation.
## Acceptance criteria
- [ ] Criterion 1
- [ ] Criterion 2
## Blocked by
- A reference to each blocking ticket, or "None — can start immediately".
</issue-template>
In either form, avoid specific file paths or code snippets — they go stale fast. Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
Work the frontier one ticket at a time with `/implement`, clearing context between tickets.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "To Tickets"
short_description: "Split a plan into tracer-bullet tickets"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,207 @@
# Writing Agent Briefs
An agent brief is a structured comment posted on a GitHub issue or PR when it moves to `ready-for-agent`. It is the authoritative specification that an AFK agent will work from. The original body and discussion are context — the agent brief is the contract.
The brief states **what the agent should do**, which stretches to both surfaces: for an issue, that's building the change from nothing; for a PR, it's what's left to do *to the existing diff* — finish it, close gaps, address review points. Same principles either way; the PR example below shows the difference.
## Principles
### Durability over precision
The issue may sit in `ready-for-agent` for days or weeks. The codebase will change in the meantime. Write the brief so it stays useful even as files are renamed, moved, or refactored.
- **Do** describe interfaces, types, and behavioral contracts
- **Do** name specific types, function signatures, or config shapes that the agent should look for or modify
- **Don't** reference file paths — they go stale
- **Don't** reference line numbers
- **Don't** assume the current implementation structure will remain the same
### Behavioral, not procedural
Describe **what** the system should do, not **how** to implement it. The agent will explore the codebase fresh and make its own implementation decisions.
- **Good:** "The `SkillConfig` type should accept an optional `schedule` field of type `CronExpression`"
- **Bad:** "Open src/types/skill.ts and add a schedule field on line 42"
- **Good:** "When a user runs `/triage` with no arguments, they should see a summary of issues needing attention"
- **Bad:** "Add a switch statement in the main handler function"
### Complete acceptance criteria
The agent needs to know when it's done. Every agent brief must have concrete, testable acceptance criteria. Each criterion should be independently verifiable.
- **Good:** "Running `gh issue list --label needs-triage` returns issues that have been through initial classification"
- **Bad:** "Triage should work correctly"
### Explicit scope boundaries
State what is out of scope. This prevents the agent from gold-plating or making assumptions about adjacent features.
## Template
```markdown
## Agent Brief
**Category:** bug / enhancement
**Summary:** one-line description of what needs to happen
**Current behavior:**
Describe what happens now. For bugs, this is the broken behavior.
For enhancements, this is the status quo the feature builds on.
**Desired behavior:**
Describe what should happen after the agent's work is complete.
Be specific about edge cases and error conditions.
**Key interfaces:**
- `TypeName` — what needs to change and why
- `functionName()` return type — what it currently returns vs what it should return
- Config shape — any new configuration options needed
**Acceptance criteria:**
- [ ] Specific, testable criterion 1
- [ ] Specific, testable criterion 2
- [ ] Specific, testable criterion 3
**Out of scope:**
- Thing that should NOT be changed or addressed in this issue
- Adjacent feature that might seem related but is separate
```
## Examples
### Good agent brief (bug)
```markdown
## Agent Brief
**Category:** bug
**Summary:** Skill description truncation drops mid-word, producing broken output
**Current behavior:**
When a skill description exceeds 1024 characters, it is truncated at exactly
1024 characters regardless of word boundaries. This produces descriptions
that end mid-word (e.g. "Use when the user wants to confi").
**Desired behavior:**
Truncation should break at the last word boundary before 1024 characters
and append "..." to indicate truncation.
**Key interfaces:**
- The `SkillMetadata` type's `description` field — no type change needed,
but the validation/processing logic that populates it needs to respect
word boundaries
- Any function that reads SKILL.md frontmatter and extracts the description
**Acceptance criteria:**
- [ ] Descriptions under 1024 chars are unchanged
- [ ] Descriptions over 1024 chars are truncated at the last word boundary
before 1024 chars
- [ ] Truncated descriptions end with "..."
- [ ] The total length including "..." does not exceed 1024 chars
**Out of scope:**
- Changing the 1024 char limit itself
- Multi-line description support
```
### Good agent brief (enhancement)
```markdown
## Agent Brief
**Category:** enhancement
**Summary:** Add `.out-of-scope/` directory support for tracking rejected feature requests
**Current behavior:**
When a feature request is rejected, the issue is closed with a `wontfix` label
and a comment. There is no persistent record of the decision or reasoning.
Future similar requests require the maintainer to recall or search for the
prior discussion.
**Desired behavior:**
Rejected feature requests should be documented in `.out-of-scope/<concept>.md`
files that capture the decision, reasoning, and links to all issues that
requested the feature. When triaging new issues, these files should be
checked for matches.
**Key interfaces:**
- Markdown file format in `.out-of-scope/` — each file should have a
`# Concept Name` heading, a `**Decision:**` line, a `**Reason:**` line,
and a `**Prior requests:**` list with issue links
- The triage workflow should read all `.out-of-scope/*.md` files early
and match incoming issues against them by concept similarity
**Acceptance criteria:**
- [ ] Closing a feature as wontfix creates/updates a file in `.out-of-scope/`
- [ ] The file includes the decision, reasoning, and link to the closed issue
- [ ] If a matching `.out-of-scope/` file already exists, the new issue is
appended to its "Prior requests" list rather than creating a duplicate
- [ ] During triage, existing `.out-of-scope/` files are checked and surfaced
when a new issue matches a prior rejection
**Out of scope:**
- Automated matching (human confirms the match)
- Reopening previously rejected features
- Bug reports (only enhancement rejections go to `.out-of-scope/`)
```
### Good agent brief (PR)
For a PR, "Current behavior" describes the state of the diff, and the brief asks the agent to finish or fix it rather than build from scratch.
```markdown
## Agent Brief
**Category:** enhancement
**Summary:** Finish the contributor's `--json` output flag for `triage list`
**Current behavior:**
The PR adds a `--json` flag that serializes the issue list to JSON. The happy
path works and the diff matches the project's command structure. Two gaps
remain: errors are still printed as human text (not JSON), and the new flag has
no test coverage.
**Desired behavior:**
With `--json`, all output — including errors — is well-formed JSON on stdout,
and the command's exit codes are unchanged. The existing human-readable output
is untouched when the flag is absent.
**Key interfaces:**
- The command's error path should emit `{ "error": string }` under `--json`
instead of the plain-text error
- Reuse the existing serializer the PR already added; don't introduce a second
**Acceptance criteria:**
- [ ] `triage list --json` emits valid JSON for both success and error cases
- [ ] Exit codes match the non-JSON command
- [ ] A test covers the `--json` success output and one error case
- [ ] Default (non-JSON) output is byte-for-byte unchanged
**Out of scope:**
- Adding `--json` to any other command
- Changing the JSON shape of the success payload the PR already defined
```
### Bad agent brief
```markdown
## Agent Brief
**Summary:** Fix the triage bug
**What to do:**
The triage thing is broken. Look at the main file and fix it.
The function around line 150 has the issue.
**Files to change:**
- src/triage/handler.ts (line 150)
- src/types.ts (line 42)
```
This is bad because:
- No category
- Vague description ("the triage thing is broken")
- References file paths and line numbers that will go stale
- No acceptance criteria
- No scope boundaries
- No description of current vs desired behavior

View File

@@ -0,0 +1,105 @@
# Out-of-Scope Knowledge Base
The `.out-of-scope/` directory in a repo stores persistent records of rejected feature requests. It serves two purposes:
1. **Institutional memory** — why a feature was rejected, so the reasoning isn't lost when the issue is closed
2. **Deduplication** — when a new issue comes in that matches a prior rejection, the skill can surface the previous decision instead of re-litigating it
## Directory structure
```
.out-of-scope/
├── dark-mode.md
├── plugin-system.md
└── graphql-api.md
```
One file per **concept**, not per issue. Multiple issues requesting the same thing are grouped under one file.
## File format
The file should be written in a relaxed, readable style — more like a short design document than a database entry. Use paragraphs, code samples, and examples to make the reasoning clear and useful to someone encountering it for the first time.
```markdown
# Dark Mode
This project does not support dark mode or user-facing theming.
## Why this is out of scope
The rendering pipeline assumes a single color palette defined in
`ThemeConfig`. Supporting multiple themes would require:
- A theme context provider wrapping the entire component tree
- Per-component theme-aware style resolution
- A persistence layer for user theme preferences
This is a significant architectural change that doesn't align with the
project's focus on content authoring. Theming is a concern for downstream
consumers who embed or redistribute the output.
```ts
// The current ThemeConfig interface is not designed for runtime switching:
interface ThemeConfig {
colors: ColorPalette; // single palette, resolved at build time
fonts: FontStack;
}
```
## Prior requests
- #42 — "Add dark mode support"
- #87 — "Night theme for accessibility"
- #134 — "Dark theme option"
```
### Naming the file
Use a short, descriptive kebab-case name for the concept: `dark-mode.md`, `plugin-system.md`, `graphql-api.md`. The name should be recognizable enough that someone browsing the directory understands what was rejected without opening the file.
### Writing the reason
The reason should be substantive — not "we don't want this" but why. Good reasons reference:
- Project scope or philosophy ("This project focuses on X; theming is a downstream concern")
- Technical constraints ("Supporting this would require Y, which conflicts with our Z architecture")
- Strategic decisions ("We chose to use A instead of B because...")
The reason should be durable. Avoid referencing temporary circumstances ("we're too busy right now") — those aren't real rejections, they're deferrals.
## When to check `.out-of-scope/`
During triage (Step 1: Gather context), read all files in `.out-of-scope/`. When evaluating a new issue:
- Check if the request matches an existing out-of-scope concept
- Matching is by concept similarity, not keyword — "night theme" matches `dark-mode.md`
- If there's a match, surface it to the maintainer: "This is similar to `.out-of-scope/dark-mode.md` — we rejected this before because [reason]. Do you still feel the same way?"
The maintainer may:
- **Confirm** — the new issue gets added to the existing file's "Prior requests" list, then closed
- **Reconsider** — the out-of-scope file gets deleted or updated, and the issue proceeds through normal triage
- **Disagree** — the issues are related but distinct, proceed with normal triage
## When to write to `.out-of-scope/`
Only when an **enhancement** (not a bug) is *rejected* as `wontfix`. This applies to enhancement PRs exactly as it does to issues — a rejected PR is recorded here so the same request doesn't return as fresh code.
Do **not** write here when something is closed as `wontfix` because it's **already implemented**. That's a built feature, not a rejected one; recording it would poison the dedup checks with false rejections. Instead, the closing comment points to where the feature already lives.
The flow:
1. Maintainer decides a feature request is out of scope
2. Check if a matching `.out-of-scope/` file already exists
3. If yes: append the new issue to the "Prior requests" list
4. If no: create a new file with the concept name, decision, reason, and first prior request
5. Post a comment on the issue explaining the decision and mentioning the `.out-of-scope/` file
6. Close the issue with the `wontfix` label
## Updating or removing out-of-scope files
If the maintainer changes their mind about a previously rejected concept:
- Delete the `.out-of-scope/` file
- The skill does not need to reopen old issues — they're historical records
- The new issue that triggered the reconsideration proceeds through normal triage

View File

@@ -0,0 +1,112 @@
---
name: triage
description: Move issues and external PRs through a state machine of triage roles — categorise, verify, grill if needed, and write agent-ready briefs.
disable-model-invocation: true
---
# Triage
Move issues on the project issue tracker through a small state machine of triage roles.
If this repo treats external pull requests as a request surface (see the issue-tracker config), triage covers them too: **a PR is an issue with attached code** — same roles, same states, same machine, with a few deltas marked "for a PR" below. Resolve a bare `#42` to an issue or PR per the tracker config.
Every comment or issue posted to the issue tracker during triage **must** start with this disclaimer:
```
> *This was generated by AI during triage.*
```
## Reference docs
- [AGENT-BRIEF.md](AGENT-BRIEF.md) — how to write durable agent briefs
- [OUT-OF-SCOPE.md](OUT-OF-SCOPE.md) — how the `.out-of-scope/` knowledge base works
## Roles
Two **category** roles:
- `bug` — something is broken
- `enhancement` — new feature or improvement
Five **state** roles:
- `needs-triage` — maintainer needs to evaluate
- `needs-info` — waiting on reporter for more information
- `ready-for-agent` — fully specified, ready for an AFK agent
- `ready-for-human` — needs human implementation
- `wontfix` — will not be actioned
For a PR, the same states read against the attached code: `ready-for-agent` means a brief is attached and an agent should take the next step on the diff; `ready-for-human` means it's ready for a human to merge.
Every triaged issue should carry exactly one category role and one state role. If state roles conflict, flag it and ask the maintainer before doing anything else.
These are canonical role names — the actual label strings used in the issue tracker may differ. The mapping should have been provided to you - run `/setup-matt-pocock-skills` if not.
State transitions: an unlabeled issue normally goes to `needs-triage` first; from there it moves to `needs-info`, `ready-for-agent`, `ready-for-human`, or `wontfix`. `needs-info` returns to `needs-triage` once the reporter replies. The maintainer can override at any time — flag transitions that look unusual and ask before proceeding.
## Invocation
The maintainer invokes `/triage` and describes what they want in natural language. Interpret the request and act. Examples:
- "Show me anything that needs my attention"
- "Let's look at #42" (issue or PR)
- "Move #42 to ready-for-agent"
- "What's ready for agents to pick up?"
## Show what needs attention
Query the issue tracker and present three buckets, oldest first:
1. **Unlabeled** — never triaged.
2. **`needs-triage`** — evaluation in progress.
3. **`needs-info` with reporter activity since the last triage notes** — needs re-evaluation.
When PRs are in scope, include external PRs in these buckets and tag each line `[PR]` or `[issue]`. Discovery surfaces only *external* PRs (the tracker config defines who counts as external) — a collaborator's in-flight PR is not triage work. This filter is discovery-only; an explicitly named PR is always triaged regardless of author.
Show counts and a one-line summary per item. Let the maintainer pick.
## Triage a specific issue or PR
1. **Gather context.** Read the full issue or PR (body, comments, labels, author, dates; for a PR, the diff too). Parse any prior triage notes so you don't re-ask resolved questions. Explore the codebase using the project's domain glossary, respecting ADRs in the area. Run two checks against the codebase: (a) **redundancy** — search for an existing implementation of the requested behavior by domain concept (not just the request's wording), and report where you looked. If found, it's an already-implemented `wontfix` (step 5). (b) **prior rejection** — read `.out-of-scope/*.md` and surface any that resembles this request.
2. **Recommend.** Tell the maintainer your category and state recommendation with reasoning, plus a brief codebase summary relevant to the request — including whether it's already implemented. Wait for direction.
3. **Verify the claim.** Before any grilling, check that the claim holds up. For a bug, reproduce it from the reporter's steps. For a PR, confirm the diff does what it claims — check it out, run the relevant tests or commands. Report what happened: confirmed (with code path), failed, or insufficient detail (a strong `needs-info` signal). A confirmed verification makes a much stronger agent brief.
4. **Grill (if needed).** If the request needs fleshing out, run the `/grilling` and `/domain-modeling` skills together — grill it into shape one question at a time, sharpening domain terms and updating `CONTEXT.md`/ADRs inline as decisions land.
5. **Apply the outcome:**
- `ready-for-agent` — post an agent brief comment ([AGENT-BRIEF.md](AGENT-BRIEF.md)).
- `ready-for-human` — same structure as an agent brief, but note why it can't be delegated (judgment calls, external access, design decisions, manual testing).
- `needs-info` — post triage notes (template below).
- `wontfix` — close, with the comment depending on *why*:
- **Already implemented** — the change already exists in the codebase. Point to where it lives; do **not** write to `.out-of-scope/` (that KB is for *rejected* requests, not built ones).
- **Rejected (bug)** — polite explanation, then close.
- **Rejected (enhancement)** — write to `.out-of-scope/`, link to it from a comment, then close ([OUT-OF-SCOPE.md](OUT-OF-SCOPE.md)).
- `needs-triage` — apply the role. Optional comment if there's partial progress.
## Quick state override
If the maintainer says "move #42 to ready-for-agent", trust them and apply the role directly. Confirm what you're about to do (role changes, comment, close), then act. Skip grilling. If moving to `ready-for-agent` without a grilling session, ask whether they want to write an agent brief.
## Needs-info template
```markdown
## Triage Notes
**What we've established so far:**
- point 1
- point 2
**What we still need from you (@reporter):**
- question 1
- question 2
```
Capture everything resolved during grilling under "established so far" so the work isn't lost. Questions must be specific and actionable, not "please provide more info".
## Resuming a previous session
If prior triage notes exist on the issue or PR, read them, check whether the reporter has answered any outstanding questions, and present an updated picture before continuing. Don't re-ask resolved questions.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Triage"
short_description: "Move issues through triage roles"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,128 @@
---
name: wayfinder
description: Plan a huge chunk of work — more than one agent session can hold — as a shared map of decision tickets on your issue tracker, and resolve them one at a time until the way to the destination is clear.
disable-model-invocation: true
---
A loose idea has arrived — too big for one agent session, and wrapped in fog: the way from here to the **destination** isn't visible yet. Wayfinding is about finding that way, not charging at the destination. This skill charts the way as a **shared map** on the repo's issue tracker, then works its **decision tickets** — questions whose resolution is a decision, not slices of a build to execute — one at a time until the route is clear.
The destination varies per effort, and naming it is the first act of charting — it shapes every ticket. It might be a spec to hand off and iterate on, a decision to lock before planning starts, or a change made in place like a data-structure migration. The map is domain-agnostic — engineering work, course content, whatever fits the shape.
## Plan, don't do
Wayfinder is **planning** by default: each ticket resolves a decision, and the map is done when the way is clear — nothing left to decide before someone goes and does the thing. The pull to just do the work is usually the signal you've reached the edge of the map and it's time to hand off. An effort can override this in its **Notes** — carrying execution into the map itself — but absent that, produce decisions, not deliverables.
## Refer by name
Every map and ticket is an issue, so it has a **name** — its title. In everything the human reads — narration, the map's Decisions-so-far — refer to it by that name, never by a bare id, number, or slug. A wall of `#42, #43, #44` is illegible; names read at a glance. The id and URL don't vanish — a name wraps its link — but they ride *inside* the name, never stand in for it.
## The Map
The map is a single issue on this repo's issue tracker, labelled `wayfinder:map` — the canonical artifact. Its tickets are child issues of the map.
The map is an **index**, not a store. It lists the decisions made and points at the tickets that hold their detail; a decision lives in exactly one place — its ticket — so the map never restates it, only gists it and links.
**Where the map, its child tickets, blocking, and frontier queries physically live is tracker-specific.** The issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if not. Consult the tracker doc's "Wayfinding operations" section for how _this_ repo expresses them. If no tracker has been provided, default to the local-markdown tracker.
### The map body
The whole map at low resolution, loaded once per session. Open tickets are **not** listed — they are open child issues, found by query.
```markdown
## Destination
<what reaching the end of this map looks like — the spec, decision, or change this effort is finding its way to. One or two lines; every session orients to it before choosing a ticket.>
## Notes
<domain; skills every session should consult; standing preferences for this effort>
## Decisions so far
<!-- the index — one line per closed ticket: enough to judge relevance, then zoom the link for the detail the ticket holds -->
- [<closed ticket title>](link) — <one-line gist of the answer>
## Not yet specified
<!-- see "Fog of war": in-scope fog you can't ticket yet; graduates as the frontier advances -->
## Out of scope
<!-- see "Out of scope": work ruled beyond the destination; closed, never graduates -->
```
### Tickets
Each ticket is a **child issue** of the map; the tracker's issue id is its identity. Its body is the question, sized to one 100K token agent session:
```markdown
## Question
<the decision or investigation this ticket resolves>
```
Each ticket carries a `wayfinder:<type>` label — one of `research`, `prototype`, `grilling`, `task` (see [Ticket Types](#ticket-types)).
A session **claims** a ticket by assigning it to the dev driving the map, **first**, before any work, so concurrent sessions skip it. That assignee _is_ the claim: an open, unassigned ticket is unclaimed.
Blocking uses the tracker's **native** dependency relationship — essential because it renders the frontier _visually_ in the tracker's own UI, so the human sees what's takeable without opening the map. Only a tracker that lacks native blocking falls back to a body convention. A ticket is **unblocked** when every ticket blocking it is closed; the **frontier** is the open, unblocked, unclaimed children — the edge of the known.
The answer isn't part of the body — it's recorded on resolution (see [Work through the map](#work-through-the-map)). Assets created while resolving a ticket are linked from the issue, not pasted in.
## Ticket Types
Every ticket is either **HITL** — human in the loop, worked *with* a human who speaks for themselves — or **AFK**, driven by the agent alone. A HITL ticket only resolves through that live exchange; the agent never stands in for the human's side of it (a grilling agent that answers its own questions has broken this).
- **Research** (AFK): Reading documentation, third-party APIs, or local resources like knowledge bases to surface a fact a decision waits on. Resolved by a `/research` **subagent**. Use when knowledge outside the current working directory is required.
- **Prototype** (HITL): Raise the fidelity of the discussion by making a cheap, rough, concrete artifact to react to — an outline, a rough take, a stub, or UI/logic code via the /prototype skill. Links the prototype as an asset. Use when "how should it look" or "how should it behave" is the key question.
- **Grilling** (HITL): Conversation via the /grilling and /domain-modeling skills, one question at a time. The default case.
- **Task** (HITL or AFK): Manual work that must happen before a *decision* can be made — nothing to decide, prototype, or research, but the discussion is blocked until it's done. Signing up for a service so its API can be judged, provisioning access, moving data so its shape can be seen. This is the one type that *does* rather than decides — and it earns its place by unblocking a decision, not by delivering the destination. The agent drives it alone where it can (AFK); otherwise it hands the human a precise checklist (HITL). Resolved when the work is done; the answer records what was done and any resulting facts (credentials location, new URLs, row counts) later tickets depend on.
## Fog of war
The map is _deliberately_ incomplete: don't chart what you can't yet see. Beyond the live tickets lies the **fog of war** — the dim view of decisions and investigations you can tell are coming but can't yet pin down, because they hang on questions still open. Resolving a ticket clears the fog ahead of it, graduating whatever's now specifiable into fresh tickets — one at a time, until the way to the destination is clear and no tickets remain.
The map's **Not yet specified** section is where that dim view is written down: the suspected question, the area to revisit later. It's the undiscovered frontier _toward_ the destination — everything here is in scope, just not sharp enough to ticket. Write as loosely or as fully as the view allows; it doubles as a signpost for collaborators reading where the effort is headed.
**Fog or ticket?** The test is whether you can state the question precisely now — _not_ whether you can answer it now.
- **Ticket when** the question is already sharp — even if it's blocked and you can't act on it yet.
- **Not yet specified when** you can't yet phrase it that sharply. Don't pre-slice the fog into ticket-sized pieces: it's coarser than a ticket, and one patch may graduate into several tickets, or none, once the frontier reaches it.
**Not yet specified** excludes what's already decided (Decisions so far), what's already a live ticket, and what's out of scope (the next section).
## Out of scope
Fog only ever gathers _toward_ the destination. The destination fixes the scope, so work beyond it is **out of scope** — it isn't fog, and it doesn't belong in **Not yet specified**. It gets its own **Out of scope** section on the map: work you've consciously ruled out of _this_ effort. Scope, not sharpness, lands it here.
Out-of-scope work never graduates — the frontier stops at the destination — so it returns only if the destination is redrawn, and then as a fresh effort, not a resumption.
Ruling something out of scope is a scoping act, not a step on the route. When a ticket that already exists turns out to sit past the destination — mis-scoped in while charting, or exposed by a resolution — **close it** (a closed ticket is unambiguously off the frontier) and leave one line in the **Out of scope** section: the gist plus why it's out of scope, linking the closed ticket. It stays out of **Decisions so far**, which records the route actually walked — a scope boundary isn't a step on it.
## Invocation
Two modes. Either way, **never resolve more than one ticket per session** — with the exception of research tickets.
### Chart the map
User invokes with a loose idea.
1. **Name the destination.** Run a `/grilling` and `/domain-modeling` session to pin down what this map is finding its way to — the spec, decision, or change. The destination fixes the scope, so it's settled first.
2. **Map the frontier.** Grill again, **breadth-first** this time: fan out across the whole space rather than deep on any one thread, surfacing the open decisions and the first steps takeable now. **If this surfaces no fog** — the way to the destination is already clear, the whole journey small enough for one session — you don't need a map. Stop and ask the user how they'd like to proceed.
3. **Create the map** (label `wayfinder:map`): Destination and Notes filled in, Decisions-so-far empty, the fog sketched into **Not yet specified**.
4. **Create the tickets you can specify now** as child issues of the map — then wire blocking edges in a **second pass** (issues need ids before they can reference each other). Wiring sorts them into the frontier and the blocked; everything you can't yet specify stays in the fog — the **Not yet specified** section.
5. **Fire the research subagents.** For each `research` ticket you just created, spin up a `/research` subagent to resolve it in parallel, capturing its findings on a throwaway `research/<name>` branch with a context pointer from the ticket.
6. Stop — charting is one session's work; it hand-resolves nothing.
### Work through the map
User invokes with a map (URL or number). A ticket is **optional** — without one, you pick the next decision, not the user.
1. Load the **map** — the low-res view, not every ticket body.
2. Choose the ticket. If the user named one, use it. Otherwise take the first frontier ticket in order. **Claim it**: assign it to yourself before any work.
3. Resolve it — **zoom as needed**: fetch the full body of any related or closed ticket on demand; invoke the skills the `## Notes` block names. If in doubt, use `/grilling` and `/domain-modeling`.
4. Record the resolution: post the answer as a **resolution comment**, **close** the issue, and **append a context pointer** to the map's Decisions-so-far.
5. Add newly-surfaced tickets (create-then-wire); graduate any fog the answer has made specifiable, clearing each graduated patch from **Not yet specified** so it lives only as its new ticket. If the answer reveals a ticket — this one or another — sits beyond the destination, **rule it out of scope** rather than resolving it on the route. If the decision invalidates other parts of the map, update or delete those tickets.
The user may run unblocked tickets in parallel, so expect other sessions to be editing the tracker concurrently.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Wayfinder"
short_description: "Map a large effort as decision tickets"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,117 @@
---
name: write-a-skill
description: Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.
---
# Writing Skills
## Process
1. **Gather requirements** - ask user about:
- What task/domain does the skill cover?
- What specific use cases should it handle?
- Does it need executable scripts or just instructions?
- Any reference materials to include?
2. **Draft the skill** - create:
- SKILL.md with concise instructions
- Additional reference files if content exceeds 500 lines
- Utility scripts if deterministic operations needed
3. **Review with user** - present draft and ask:
- Does this cover your use cases?
- Anything missing or unclear?
- Should any section be more/less detailed?
## Skill Structure
```
skill-name/
├── SKILL.md # Main instructions (required)
├── REFERENCE.md # Detailed docs (if needed)
├── EXAMPLES.md # Usage examples (if needed)
└── scripts/ # Utility scripts (if needed)
└── helper.js
```
## SKILL.md Template
```md
---
name: skill-name
description: Brief description of capability. Use when [specific triggers].
---
# Skill Name
## Quick start
[Minimal working example]
## Workflows
[Step-by-step processes with checklists for complex tasks]
## Advanced features
[Link to separate files: See [REFERENCE.md](REFERENCE.md)]
```
## Description Requirements
The description is **the only thing your agent sees** when deciding which skill to load. It's surfaced in the system prompt alongside all other installed skills. Your agent reads these descriptions and picks the relevant skill based on the user's request.
**Goal**: Give your agent just enough info to know:
1. What capability this skill provides
2. When/why to trigger it (specific keywords, contexts, file types)
**Format**:
- Max 1024 chars
- Write in third person
- First sentence: what it does
- Second sentence: "Use when [specific triggers]"
**Good example**:
```
Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when user mentions PDFs, forms, or document extraction.
```
**Bad example**:
```
Helps with documents.
```
The bad example gives your agent no way to distinguish this from other document skills.
## When to Add Scripts
Add utility scripts when:
- Operation is deterministic (validation, formatting)
- Same code would be generated repeatedly
- Errors need explicit handling
Scripts save tokens and improve reliability vs generated code.
## When to Split Files
Split into separate files when:
- SKILL.md exceeds 100 lines
- Content has distinct domains (finance vs sales schemas)
- Advanced features are rarely needed
## Review Checklist
After drafting, verify:
- [ ] Description includes triggers ("Use when...")
- [ ] SKILL.md under 100 lines
- [ ] No time-sensitive info
- [ ] Consistent terminology
- [ ] Concrete examples included
- [ ] References one level deep

View File

@@ -0,0 +1,201 @@
# Glossary — Building Great Skills
The domain model for what makes a skill great. A skill exists to wrangle determinism out of a stochastic system; the root virtue is **Predictability**, and every term below is a lever on it. This is the disclosed reference for [`writing-great-skills`](SKILL.md).
The terms are grouped by axis: **Invocation** (how a skill is reached), **Information Hierarchy** (how its content is arranged), **Steering** (how the agent's runtime behaviour is shaped), and **Pruning** (how it is kept lean). Each **failure mode** lives beside the lever that cures it, tagged _failure mode_.
**Bold terms** in any definition are themselves defined in this glossary; find them by their heading.
## Predictability
The degree to which a skill makes the agent behave the same _way_ on every run — the same process, not the same output (a brainstorming skill should _predictably_ diverge; its tokens vary, its behaviour doesn't). The root virtue every other term serves — cost and maintainability are symptoms of it, not rivals.
_Avoid_: consistency, reliability, robustness, output-determinism
## Invocation
How a skill is reached — and the two loads you pay for the choice.
### Model-Invoked
A skill that keeps its **description** field, so the agent can see it and fire it autonomously — and the human can still type its name, so model-invocation always _includes_ user reach. There is no model-only state: a description only ever _adds_ agent discovery, never removes the human's. Pays a permanent **context load** on every turn in exchange for that discoverability. Reachable by other skills, because the description that makes it agent-discoverable makes it invocable. A model-invoked skill whose content is all **reference** is also one home for shared reference: another skill can invoke it, so reference needed by several skills lives in one place. Pick model-invocation only when the agent must reach the skill on its own; if it never fires except by hand, drop the description and pay no context load.
_Avoid_: ability, tool, capability
### User-Invoked
A skill with its **description** stripped — invisible to the agent and reachable only by the human typing its name (user-_only_, where **model-invoked** is user-_and-agent_). Trades agent-discoverability for zero **context load**. Because it has no description, nothing but the human can reach it: no other skill can fire it.
_Avoid_: procedure, workflow, command
### Description
The skill's machine-readable trigger, and the one **context pointer** a **model-invoked** skill is forced to keep loaded at all times. Its mere presence _is_ the invocation axis: keep it and the skill is model-invoked (and reachable by other skills); delete it and the skill is **user-invoked**, reachable only by the human. The source of a model-invoked skill's **context load**.
_Avoid_: frontmatter, summary
### Context Pointer
A reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. The **description** is the top-level context pointer (context window → skill); pointers to disclosed files are the same object one level down. Its wording, not the target, decides _when_ the agent reaches — and _how reliably_. A must-have target behind a weakly worded pointer is a variance bug: fix the wording first, and inline the material only if sharpening fails.
_Avoid_: link, reference, import
### Context Load
The cost a **model-invoked** skill imposes on the agent's context window — its **description**, always loaded, spending both tokens and attention. What **user-invoked** skills escape by having no description, and the brake on splitting into more model-invoked skills.
_Avoid_: token cost, context bloat
### Cognitive Load
The cost a **user-invoked** skill imposes on the human — what they must hold in their head: which skills exist and when to reach for each (the human is the index). What **model-invocation** removes by being agent-discoverable, and the brake on splitting into more user-invoked skills. Not a cost to minimise: it is the price of human agency, the reason some skills stay user-invoked. Spend it where human judgement matters; remove it where it does not.
_Avoid_: human index, burden, overhead
### Router Skill
A **user-invoked** skill whose job is to point at your other user-invoked skills — naming each and when to reach for it — so the human has one skill to remember instead of many. It can only hint, never fire them: user-invoked skills have no **description**, so nothing but the human can reach them. The cure for **cognitive load** when user-invoked skills multiply.
_Avoid_: dispatcher, menu, registry, index, router procedure
### Granularity
How finely you divide skills. Finer division spends one of the two loads: more **model-invoked** skills spend **context load** (more descriptions crowding the window and competing for attention); more **user-invoked** skills spend **cognitive load** (more for the human to remember and reach for). Two cuts guide the division. By **invocation**, split off a model-invoked skill where you have a distinct **leading word** to trigger it — a trigger word you actually use in your prompts. By **sequence**, split a run of **steps** where a step's **post-completion steps** need hiding, since isolating it in its own context clears what follows. Beware the reverse: merging sequences exposes each step's post-completion steps to what follows, inviting premature completion.
_Avoid_: chunking, modularity
## Information Hierarchy
How a skill's content is arranged, and how far down the ladder each piece sits.
### Information Hierarchy
A skill's content ranked by how immediately the agent needs it — a single ladder, produced by two cuts: in-file or behind a pointer, and step or reference. The rungs:
- **Steps** — in-file, primary
- **Reference**, in-file — secondary
- **Reference**, disclosed — behind a **context pointer**
A skill with no **steps** uses just the bottom two rungs — often a legitimately flat peer-set (e.g. every rule of a review on one rung), which is a fine arrangement, not a smell. The hierarchy is independent of invocation: a skill can be model- or user-invoked whether it is all steps, all reference, or both. When a skill has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip — a variance lever, not just a legibility one. Keep the top of the ladder legible; push down it whatever you can.
_Avoid_: structure, organization, layout
### Steps
The ordered actions the agent performs — when a skill has them, the primary tier of its content, and the part that earns its place in SKILL.md. Not every skill has steps: a skill can be all steps (`tdd`), all **reference** (a review), or both, independent of invocation. Every step ends on a **completion criterion**, clear or vague.
_Avoid_: workflow, instructions, choreography
### Reference
Material the agent refers to on demand — definitions, facts, parameters, examples, conditional instructions. When a skill has **steps** it is secondary to them; when a skill has none it is the entire content; or it lives outside any skill entirely — see **External Reference**. Reached via **context pointers**, and the prime candidate for **progressive disclosure**.
_Avoid_: supporting material, docs, background
### External Reference
**Reference** that lives outside the skill system — a plain file, no **description**, no **steps**, not invocable — that any skill can point at. The home for shared reference that needn't fire on its own, and the only shared home two **user-invoked** skills can use, since neither has a description and so neither can fire the other.
_Avoid_: doc, resource, knowledge base
### Progressive Disclosure
Moving **reference** down the ladder — out of SKILL.md and behind a **context pointer** — so the top stays legible. Not primarily a token optimisation; it is how the **information hierarchy** is protected. Licensed by **branching**: disclose what only some branches need, inline what every path needs, and if a pointer fires unreliably on must-have material, sharpen its wording, and pull it back inline only if that fails.
_Avoid_: lazy loading, chunking
### Co-location
Keeping the material an agent needs at once in one place — a concept's definition, rules, and caveats under a single heading, not scattered across the file — so reading one part brings its neighbours with it. The within-file companion to the **Information Hierarchy**: the hierarchy ranks _how far down_ a piece sits; co-location decides _what sits beside it_ once there. There is no formula for the right format of a body of **reference**; the test is that a skill should read like documentation written for the agent, and grouped material reads that way where scattered material does not. Distinct from **Duplication**: that repeats one meaning in two places, where scattering fragments a single meaning across many.
_Avoid_: grouping, clustering, cohesion
### Sprawl
_Failure mode._ A skill that is simply too long — too many lines in SKILL.md — independent of whether they are stale or repeated. Even an all-live, all-unique skill can sprawl. It costs readability (the agent wades through more before it can act, and attention thins across the excess), maintainability (every extra line is one more to keep **relevant**), and tokens. The cure is the **information hierarchy**: push **reference** down behind **context pointers**, and split by **branch** or sequence so each path carries only what it needs. Distinct from **sediment** (length from stale accumulation) and **duplication** (length from repeated meaning) — sprawl is length itself, whatever its cause.
_Avoid_: bloat, length, size, verbosity
## Steering
The levers that shape the agent's runtime behaviour toward **Predictability**.
### Branch
A distinct way a skill can be invoked — a case the skill handles — so different runs take different paths through it. A skill with many steps may carry many branches; a linear one has none.
_Avoid_: path, case, fork
### Leading Word
A compact concept — also called a _Leitwort_ — already living in the model's pretraining, that the agent thinks with while running the skill. It encodes a behavioural principle in the fewest possible tokens by invoking priors the model already holds (e.g. _lesson_, _proximal zone of development_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition across the skill and anchors a whole region of behaviour. Coining your own works if you define it clearly, but a made-up word recruits no priors — you pay in definition tokens what a pretrained word gives free. Reach for an existing word first.
A leading word serves **predictability** twice. In the body it anchors **execution** — the agent reaches for the same behaviour every time the concept appears, and inside flat reference it focuses attention on a class of thing to look for, recruiting the right checks each run. In the **description** it anchors **invocation** — and not only within the skill: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the skill and fires it more reliably. Word a description with the leading words you actually use when you want the skill.
_Avoid_: keyword, term, motif
### Completion Criterion
The condition that tells the agent a unit of work is done — the target it judges against. Two properties make it a lever, not just a quality. Its **clarity** (can the agent tell done from not-done?) resists **premature completion** — a vague bound ("understanding reached") lets the agent declare done and slip to the next step; this axis needs _steps_ to bite, since premature completion is a between-steps failure. Its **demand** (how much it requires) sets **legwork** — "every modified model accounted for" forces thorough work where "produce a change list" does not — and this axis is _not_ step-bound: it can bind a body of flat reference too, which is how a skill with no steps still carries an exhaustiveness bar ("every rule applied"). The strongest criteria are both checkable and exhaustive.
_Avoid_: done condition, exit condition, stopping rule
### Legwork
The work an agent does behind the scenes within a single step — reading files, exploring the codebase, making changes, digging up what it needs rather than offloading to the user. It lives below the step structure: never written as its own step, latent in the wording, controlled by the agent rather than the skill. The within-step counterpart to **post-completion steps**' across-step pull. Raised by a **leading word** (_comprehensive_, _thorough_) or a **completion criterion** that demands the work be exhaustive — including the demand axis applied to flat reference, which is what drives a skill of flat reference to cover all its rungs. Goes thin either when that demand is missing or when **premature completion** cuts the step short.
_Avoid_: scope, effort, diligence, coverage
### Post-Completion Steps
The **steps** that follow the current step. Visible, they pull the agent forward into **premature completion** — the more it sees, the stronger the tug; the defence is to hide them by splitting the sequence of steps into two.
_Avoid_: horizon, fog of war, lookahead
### Premature Completion
_Failure mode._ Ending the current step before it is genuinely done, because the agent's attention slips to being done rather than to the work. A between-steps failure: it needs **steps** to occur — a skill with no steps that quits early isn't premature completion but thin **legwork** under an unmet demand. A tug-of-war between two forces: visible **post-completion steps** (the pull forward) and the **completion criterion**'s clarity (the resistance — a sharp, checkable bar holds; a vague one gives way). Fuzziness is the necessary condition: a sharp bound resists the pull no matter how many later steps are visible, so a step that never rushes needs no defending. Two levers hold a step that does, but reach for them in order: **sharpen the bound first** — it is local and cheap. Only when the criterion is irreducibly fuzzy _and_ you actually observe the rush do you **hide the later steps** — and hiding only works across a real context boundary (a user-invoked hand-off or a subagent dispatch; an inline model-invoked call leaves the later steps in context and clears nothing). One cause of thin legwork, but distinct from it: legwork can be thin even when a step runs to full completion.
_Avoid_: premature closure, the rush, rushing, shortcutting
### Negation
_Failure mode._ Steering by prohibition — telling the agent what _not_ to do — which drags the forbidden behaviour into context and makes it _more_ available, not less. _Don't think of an elephant_, and the elephant is all there is; _never write verbose comments_, and verbosity is the pattern the agent has just read. The negation is a weak modifier the strongly-activated concept overruns, so the ban half-reads as an instruction to do the thing. Its **leading word** is the _elephant_: whatever a prohibition names into the frame. Cure: prompt the **positive** — describe the target behaviour ("write one-line comments") so the banned one is never spoken. A prohibition earns its place only as a hard guardrail on a behaviour you cannot phrase positively; even then, pair it with the positive target so attention lands on what to do.
_Avoid_: ironic rebound, don't-prompting, the pink elephant
## Pruning
Keeping a skill lean — each remedy paired with the failure it cures.
### Single Source of Truth
The desired state where each meaning lives in exactly one authoritative place, so a change to the skill's behaviour is a change in one place. **Duplication** is its violation.
_Avoid_: home, canonical location
### Duplication
_Failure mode._ The same meaning given more than one **single source of truth**. It costs maintenance (change one place, you must change the others), costs tokens, and inflates prominence — repeating a meaning weights it on the ladder past its real rank. The accidental inverse of a **leading word**, which raises attention on purpose by repeating a token, never the meaning.
_Avoid_: repetition, redundancy
### Relevance
Whether a line still bears on what the skill does — the lens for what to keep. A line loses relevance either by never bearing on the task (mere exposition, or a **branch** that should be disclosed) or by going stale: drifting out of date as the behaviour or world it describes changes. Shorter skills are easier to keep relevant, because each line is cheaper to check. Distinct from **no-op**: relevance asks whether a line bears on the task, not whether it changes behaviour.
_Avoid_: load-bearing, staleness, freshness
### Sediment
_Failure mode._ Layers of old content that settle in a skill and are never cleared, because adding feels safe and removing feels risky — so stale and irrelevant lines accumulate and you must core down through them to find what is still live. The default fate of any skill without a pruning discipline; the slow erosion of **relevance**, as opposed to **duplication**'s repeated meaning.
_Avoid_: accretion, bloat, cruft, rot
### No-Op
_Failure mode._ An instruction that changes nothing because the model already does it by default — you pay load to tell the agent what it would do anyway. The test: does a line change behaviour versus the default? A line can be perfectly **relevant** and still be a no-op. The same priors that make a **leading word** free make a no-op worthless.
A leading word is a _technique_; No-Op is a _verdict_ on a line — and they cross. A leading word too weak to beat the default is a no-op (_be thorough_ when the agent is already thorough-ish), and the fix is a stronger word that passes the verdict (_relentless_), not a different technique. So the No-Op test — does it change behaviour versus the default? — is also how you grade whether a leading word is earning its repetitions. This is model-relative, not reader-relative: two people disagreeing over whether a line is a no-op disagree about the default, and settle it by running the skill, not by debate.
_Avoid_: redundant instruction, restating the obvious, belaboring

View File

@@ -0,0 +1,83 @@
---
name: writing-great-skills
description: Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
disable-model-invocation: true
---
A skill exists to wrangle determinism out of a stochastic system. **Predictability** — the agent taking the same _process_ every run, not producing the same output — is the root virtue; every lever below serves it.
**Bold terms** are defined in [`GLOSSARY.md`](GLOSSARY.md); look them up there for the full meaning.
## Invocation
Two choices, trading different costs:
- A **model-invoked** skill keeps a **description**, so the agent can fire it autonomously _and_ other skills can reach it (you can still type its name too). It contributes to **context load** — the description sits in the window every turn. Mechanics: omit `disable-model-invocation`, and write a model-facing description with rich trigger phrasing ("Use when the user wants…, mentions…").
- A **user-invoked** skill strips the description from the agent's reach: only you, typing its name, can invoke it — and no other skill can. Zero context load, but it spends **cognitive load**: _you_ are the index that must remember it exists. Mechanics: set `disable-model-invocation: true`; the `description` becomes human-facing — a one-line summary, trigger lists stripped.
Pick model-invocation only when the agent must reach the skill on its own, or another skill must. If it only ever fires by hand, make it user-invoked and pay no context load.
When user-invoked skills multiply past what you can remember, that piled-up cognitive load is cured by a **router skill**: one user-invoked skill that names the others and when to reach for each.
## Writing the description
A model-invoked **description** does two jobs — state what the skill is, and list the **branches** that should trigger it. Every word increases **context load**, so a description earns even harder pruning than the body:
- **Front-load the skill's leading word** — the description is where it does its invocation work.
- **One trigger per branch.** Synonyms that rename a single branch are **duplication** — "build features using TDD … asks for test-first development" is one branch written twice. Collapse them; keep only genuinely distinct branches.
- **Cut identity that's already in the body.** Keep the description to triggers, plus any "when another skill needs…" reach clause.
## Information hierarchy
A skill is built from two content types — **steps** and **reference** — that mix freely: a skill can be all steps, all reference, or both. The core decision is which to use and where each sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material:
1. **In-skill step** — an ordered action in `SKILL.md`, the primary tier: what the agent does, in order. Each step ends on a **completion criterion**, the condition that tells the agent the work is done. Make it _checkable_ (can the agent tell done from not-done?) and, where it matters, _exhaustive_ ("every modified model accounted for", not "produce a change list") — a vague criterion invites **premature completion**.
2. **In-skill reference** — a definition, rule, or fact in `SKILL.md`, consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung) — a fine arrangement, not a smell. _This skill is all reference._
3. **External reference** — reference pushed out of `SKILL.md` into a separate file, reached by a **context pointer**, loaded only when the pointer fires. (Spans _disclosed_ reference — a sibling file like `GLOSSARY.md`, still part of the skill — through fully **external reference** that lives outside the skill system and any skill can point at.)
A demanding completion criterion drives thorough **legwork** — the digging the agent does within the work — whether the skill has steps or not, since "every rule applied" binds flat reference just as "every step done" binds a sequence.
Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision.
**Progressive disclosure** is the move down the ladder — out of `SKILL.md` into a linked file — so the top stays legible. Mechanics: a linked `.md` file in the skill folder, named for what it holds (this skill discloses its full definitions to `GLOSSARY.md`). Some skills are used in more than one way, and each distinct way is a **branch** — different runs taking different paths through the skill. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. A **context pointer**'s _wording_, not its target, decides when and how reliably the agent reaches the material.
Where the ladder decides _how far down_ a piece sits, **co-location** decides _what sits beside it_ once there: keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it.
## When to split
**Granularity** is how finely you divide skills, and each cut spends one of the two loads, so split only when the cut earns it. Two cuts:
- **By invocation** — split off a **model-invoked** skill when you have a distinct **leading word** that should trigger it on its own, or another skill must reach it. You pay **context load** for the new always-loaded **description**, so that independent reach has to be worth it.
- **By sequence** — split a run of **steps** when the steps still ahead (a step's **post-completion steps**) tempt the agent to rush the one in front of it (**premature completion**). Keeping them out of view encourages the agent to do more **legwork** on the current task.
## Pruning
Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit.
Check every line for **relevance**: does it still bear on what the skill does?
Then hunt **no-ops** sentence by sentence, not just line by line: run the no-op test on each sentence in isolation, and when one fails, delete the whole sentence rather than trim words from it. Be aggressive — most prose that fails should go, not be rewritten.
## Leading words
A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the skill (e.g. _lesson_, _fog of war_, _tracer bullets_). Repeated throughout the text (though not necessarily - a strong leading word might only be needed once), it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds.
It serves predictability twice. In the body it anchors _execution_: the agent reaches for the same behaviour every time the word appears. In the description it anchors _invocation_: when the same word lives in your prompts, docs, and code, the agent links that shared language to the skill and fires it more reliably.
Hunt for opportunities to refactor skills to use leading words. A triad spelled out at three sites (**duplication**), a description spending a sentence to gesture at one idea — each is a passage begging to **collapse** into a single token. Examples include:
- "fast, deterministic, low-overhead" -> _tight_ — one quality restated across a phase — into a single pretrained word (a _tight_ loop).
- "a loop you believe in" -> _red_ — converts a fuzzy gate into a binary observable state (the loop goes _red_ on the bug, or it doesn't).
You win twice over: fewer tokens, _and_ a sharper hook for the agent to hang its thinking on. Assume every skill is carrying restatements that leading words retire — go find them.
## Failure modes
Use these to diagnose issues the user may be having with the skill.
- **Premature completion** — ending a step before it's genuinely done, attention slipping to _being done_. Defence, in order: sharpen the completion criterion first (cheap, local); only if it is irreducibly fuzzy _and_ you observe the rush, hide the post-completion steps by splitting (the sequence cut).
- **Duplication** — the same meaning in more than one place. Costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank.
- **Sediment** — stale layers that settle because adding feels safe and removing feels risky. The default fate of any skill without a pruning discipline.
- **Sprawl** — a skill simply too long, even when every line is live and unique. Hurts readability and maintainability and wastes tokens. The cure is the ladder: disclose **reference** behind pointers, and split by **branch** or sequence so each path carries only what it needs.
- **No-op** — a line the model already obeys by default, so you pay load to say nothing. The test: does it change behaviour versus the default? A weak leading word (_be thorough_ when the agent is already thorough-ish) is a no-op; the fix is a stronger word (_relentless_), not a different technique.
- **Negation** — steering by prohibition backfires: _don't think of an elephant_ names the elephant and makes it more available, not less. Prompt the **positive** — state the target behaviour so the banned one is never spoken; keep a prohibition only as a hard guardrail you can't phrase positively, and even then pair it with what to do instead.

View File

@@ -0,0 +1,5 @@
interface:
display_name: "Writing Great Skills"
short_description: "Principles for predictable skills"
policy:
allow_implicit_invocation: false

View File

@@ -0,0 +1,7 @@
---
name: zoom-out
description: Tell the agent to zoom out and give broader context or a higher-level perspective. Use when you're unfamiliar with a section of code or need to understand how it fits into the bigger picture.
disable-model-invocation: true
---
I don't know this area of code well. Go up a layer of abstraction. Give me a map of all the relevant modules and callers, using the project's domain glossary vocabulary.

1
.claude/skills/ask-matt Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/ask-matt

1
.claude/skills/caveman Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/caveman

1
.claude/skills/code-review Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/code-review

View File

@@ -0,0 +1 @@
../../.agents/skills/codebase-design

View File

@@ -0,0 +1,139 @@
---
name: comment-standards
description: Go 注释规范。编写 Go 代码注释、文档注释时使用。包含包注释、结构体注释、接口注释、函数注释、内联注释的完整规范与示例。
---
# Go 注释规范
**基本原则**
- **所有注释使用中文**
- **导出符号必须有文档注释**(包、函数、方法、类型、接口、常量、变量)
- **复杂逻辑必须有实现注释**(解释"为什么",而不是"做了什么"
- **禁止废话注释**(不要用注释复述代码本身)
- **修改代码时必须同步更新注释**
---
## 包注释
每个包的入口文件(通常是主文件或 `doc.go`)必须有包注释:
```go
// Package account 提供账号管理的业务逻辑服务
// 包含账号创建、修改、删除、权限分配等功能
package account
```
## 结构体注释
所有导出结构体必须有文档注释,说明该结构体代表什么:
```go
// Service 账号业务服务
// 负责账号的 CRUD、角色分配、密码管理等业务逻辑
type Service struct {
store *Store
auditService AuditServiceInterface
}
```
## 接口注释
导出接口必须注释接口用途,每个方法必须说明契约:
```go
// PermissionChecker 权限检查器接口
// 用于查询用户的权限列表
type PermissionChecker interface {
// CheckPermission 检查用户是否拥有指定权限
// userID: 用户ID
// permCode: 权限编码(格式: module:action
// platform: 端口类型 (all/web/h5)
CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error)
}
```
## 函数和方法注释
**导出函数/方法**必须以函数名开头,说明功能:
```go
// Create 创建账号
// POST /api/admin/accounts
func (h *AccountHandler) Create(c *fiber.Ctx) error {
```
**复杂方法**(超过 30 行或包含复杂业务逻辑)必须额外说明实现思路:
```go
// ActivateByRealname 首次实名激活套餐
// 当用户完成实名认证后,自动激活处于"囤货待实名"状态的套餐:
// 1. 查找该卡所有 status=3待实名激活的套餐
// 2. 按创建时间排序第一个主套餐立即激活status=1
// 3. 其余主套餐进入排队状态status=4
// 4. 加油包如果绑定了已激活的主套餐则一并激活
func (s *UsageService) ActivateByRealname(ctx context.Context, cardID uint) error {
```
**未导出函数/方法**
- 简单逻辑(< 15 行):可以不加注释
- 复杂逻辑(≥ 15 行)或非显而易见的算法:必须加注释
```go
// buildPermissionTree 递归构建权限树
// 采用 map 索引 + 单次遍历算法,时间复杂度 O(n)
func (s *Service) buildPermissionTree(permissions []*model.Permission) []*dto.PermissionTreeNode {
```
## 常量和枚举注释
分组常量必须有组注释,每个值必须有行内注释:
```go
// 用户类型常量
const (
UserTypeSuperAdmin = 1 // 超级管理员
UserTypePlatform = 2 // 平台用户
UserTypeAgent = 3 // 代理账号
UserTypeEnterprise = 4 // 企业账号
)
```
## 内联注释规范
**必须添加内联注释的场景**
| 场景 | 要求 |
|------|------|
| 复杂条件判断 | 解释判断的业务含义 |
| 多步骤业务流程 | 用编号注释标明每一步 |
| 非显而易见的设计决策 | 解释"为什么这样做"而不是"做了什么" |
| 缓存/事务/并发处理 | 说明策略和原因 |
| 临时方案/兼容逻辑 | 标注 TODO 或说明背景 |
**✅ 好的内联注释(解释为什么)**
```go
// 使用 Redis 分布式锁防止并发重复创建,锁超时 10 秒
if !s.acquireLock(ctx, lockKey, 10*time.Second) {
return errors.New(errors.CodeTooManyRequests, "操作过于频繁,请稍后重试")
}
// 先冻结佣金再扣款,保证资金安全(失败时佣金自动解冻)
if err := s.freezeCommission(ctx, tx, orderID); err != nil {
return err
}
```
**❌ 废话注释(禁止)**
```go
// 获取用户ID ← 禁止:代码本身已经很清楚
userID := middleware.GetUserIDFromContext(ctx)
// 创建账号 ← 禁止:变量名已说明意图
account := &model.Account{}
// 返回错误 ← 禁止return err 不需要注释
return err
```

1
.claude/skills/diagnose Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/diagnose

View File

@@ -0,0 +1 @@
../../.agents/skills/diagnosing-bugs

View File

@@ -0,0 +1 @@
../../.agents/skills/domain-modeling

View File

@@ -14,6 +14,8 @@ description: DTO 数据传输对象规范。创建或修改 DTO 文件、请求/
- 创建 `XXXRequest``XXXResponse``XXXReq``XXXResp` 结构体 - 创建 `XXXRequest``XXXResponse``XXXReq``XXXResp` 结构体
- 添加或修改 API 接口的输入输出参数 - 添加或修改 API 接口的输入输出参数
---
## 必须项MUST ## 必须项MUST
### 1. Description 标签规范 ### 1. Description 标签规范
@@ -36,59 +38,150 @@ type CreateUserRequest struct {
} }
``` ```
### 2. 枚举字段必须列出所有可能值(中文) ### 2. 枚举字段int vs string 选择
**所有枚举类型字段必须在 `description` 中列出所有可能值和对应的中文含义** **必须按以下规则选择类型,禁止混用:**
| 场景 | 类型 | 示例 |
|------|------|------|
| 状态类(生命周期阶段) | `int` | 待支付→已完成→已关闭 |
| 布尔状态(启用/禁用) | `int` | `0=禁用, 1=启用` |
| 类型/方式类(种类) | `string` | `"wechat"`, `"single_card"` |
| 平台/标识符类 | `string` | `"web"`, `"h5"`, `"all"` |
```go ```go
// 用户类型 // ✅ 状态 → int
UserType int `json:"user_type" description:"用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)"` Status int `json:"status"`
PaymentStatus int `json:"payment_status"`
// 角色类型 // ✅ 类型/方式 → string
RoleType int `json:"role_type" description:"角色类型 (1:平台角色, 2:客户角色)"` PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat offline"`
OrderType string `json:"order_type" validate:"required,oneof=single_card device"`
// 权限类型
PermType int `json:"perm_type" description:"权限类型 (1:菜单, 2:按钮)"`
// 状态字段
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
// 适用端口
Platform string `json:"platform" description:"适用端口 (all:全部, web:Web后台, h5:H5端)"`
``` ```
**禁止使用英文枚举值** ### 3. Int 状态值约定
#### 3.1 通用禁用/启用
**必须用全局常量,禁止自定义(尤其禁止 1=启用 2=禁用 这种反向写法)**
```go ```go
UserType int `json:"user_type" description:"用户类型 (1:SuperAdmin, 2:Platform)"` // 错误! // pkg/constants/constants.go 已定义,直接使用
StatusDisabled = 0 // 禁用
StatusEnabled = 1 // 启用
``` ```
### 3. 验证标签与 OpenAPI 标签一致 ✅ 正确:`description:"状态 (0:禁用, 1:启用)"`
❌ 禁止:`description:"状态 (1:启用, 2:禁用)"`
**所有验证约束必须同时在 `validate` 和 OpenAPI 标签中声明** #### 3.2 生命周期状态
**1** 开始递增0 不使用(避免与 Go 零值混淆):
```go
const (
RechargeStatusPending = 1 // 待支付
RechargeStatusPaid = 2 // 已支付
RechargeStatusCompleted = 3 // 已完成
RechargeStatusClosed = 4 // 已关闭
)
```
### 4. 枚举列表必须从 constants 原文抄写
**DTO description 的枚举列表必须与 `pkg/constants/` 定义完全一致,不可凭记忆填写。**
操作步骤:
1. 先查/定义 `pkg/constants/` 中的枚举常量
2. 将常量注释**原文抄写**到 description
```go
// constants.go 中:
RechargeStatusPending = 1 // 待支付
RechargeStatusPaid = 2 // 已支付
RechargeStatusCompleted = 3 // 已完成
RechargeStatusClosed = 4 // 已关闭
RechargeStatusRefunded = 5 // 已退款
// DTO description 从上面抄:
Status int `json:"status" description:"状态 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款)"`
```
❌ 禁止description 与 constants 不一致,是历史 bug 的根因):
```go
// constants 说 3=已完成description 却写 3:已取消
Status int `json:"status" description:"状态 (1:待支付, 2:已完成, 3:已取消)"`
```
### 5. description 格式标准
**统一格式**`字段含义 (值1:中文含义1, 值2:中文含义2)`
- 值与含义之间用**冒号** `:`(禁止用等号 `=`
- 多个值之间用**逗号加空格** `, `
- 含义必须是**中文**
```go
// ✅ 统一格式
Status int `description:"状态 (1:待支付, 2:已支付, 3:已完成)"`
Platform string `description:"适用端口 (all:全部, web:Web后台, h5:H5端)"`
// ❌ 格式混乱
Status int `description:"状态 (0=禁用, 1=启用)"` // 用等号
Status int `description:"0=禁用 1=启用"` // 无括号无逗号
```
### 6. Response DTO 的状态字段必须同时返回 int 和 text
**所有 Response DTO 中的 int 状态字段,必须同时提供对应的 `_name` 文字字段。**
原因:防止前端维护映射表出错(历史上已有因此产生 bug 的案例)。
```go
// ✅ Response DTO 标准写法
type XxxResponse struct {
Status int `json:"status" description:"状态 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款)"`
StatusName string `json:"status_name" description:"状态名称(中文)"`
}
// toResponse 函数中赋值
func rechargeStatusName(status int) string {
switch status {
case constants.RechargeStatusPending:
return "待支付"
case constants.RechargeStatusCompleted:
return "已完成"
// ...
default:
return "未知"
}
}
```
字段命名约定:`status``status_name``payment_status``payment_status_name`
**例外**Request DTO查询过滤、创建请求不需要 `_name` 字段。
### 7. 验证标签与 OpenAPI 标签一致
```go ```go
Username string `json:"username" validate:"required,min=3,max=50" required:"true" minLength:"3" maxLength:"50" description:"用户名"` Username string `json:"username" validate:"required,min=3,max=50" required:"true" minLength:"3" maxLength:"50" description:"用户名"`
``` ```
**标签对照表** | validate 标签 | OpenAPI 标签 |
|--------------|--------------|
| `required` | `required:"true"` |
| `min=N,max=M`(数值) | `minimum:"N" maximum:"M"` |
| `min=N,max=M`(字符串) | `minLength:"N" maxLength:"M"` |
| `oneof=A B C` | description 中说明枚举值 |
| validate 标签 | OpenAPI 标签 | 说明 | ### 8. 请求参数类型标签
|--------------|--------------|------|
| `required` | `required:"true"` | 必填字段 |
| `min=N,max=M` | `minimum:"N" maximum:"M"` | 数值范围 |
| `min=N,max=M` (字符串) | `minLength:"N" maxLength:"M"` | 字符串长度 |
| `len=N` | `minLength:"N" maxLength:"N"` | 固定长度 |
| `oneof=A B C` | `description` 中说明 | 枚举值 |
### 4. 请求参数类型标签
**Query 参数和 Path 参数必须添加对应标签**
```go ```go
// Query 参数 // Query 参数
type ListRequest struct { type ListRequest struct {
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"` Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
UserType *int `json:"user_type" query:"user_type" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)"` Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:待支付, 2:已支付, 3:已完成, 4:已关闭)"`
} }
// Path 参数 // Path 参数
@@ -97,9 +190,7 @@ type IDReq struct {
} }
``` ```
### 5. 响应 DTO 完整性 ### 9. 响应 DTO 完整性
**所有响应 DTO 的字段都必须有完整的 `description` 标签**
```go ```go
type AccountResponse struct { type AccountResponse struct {
@@ -107,33 +198,42 @@ type AccountResponse struct {
Username string `json:"username" description:"用户名"` Username string `json:"username" description:"用户名"`
UserType int `json:"user_type" description:"用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)"` UserType int `json:"user_type" description:"用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)"`
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"` Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
StatusName string `json:"status_name" description:"状态名称(中文)"`
CreatedAt string `json:"created_at" description:"创建时间"` CreatedAt string `json:"created_at" description:"创建时间"`
UpdatedAt string `json:"updated_at" description:"更新时间"` UpdatedAt string `json:"updated_at" description:"更新时间"`
} }
``` ```
---
## AI 助手必须执行的检查 ## AI 助手必须执行的检查
**在创建或修改任何 DTO 文件后,必须执行以下检查:** **在创建或修改任何 DTO 文件后,必须执行以下检查:**
1.检查所有字段是否`description` 标签 1. ✅ 所有字段有 `description` 标签(无行内注释)
2.检查枚举字段是否列出了所有可能值(中文 2.枚举类型选择正确(状态用 int类型/方式用 string
3.检查状态字段是否说明了 0 和 1 的含义 3.禁用/启用使用 `0=禁用, 1=启用`(禁止 1=启用 2=禁用)
4.检查 validate 标签与 OpenAPI 标签是否一致 4.description 枚举列表已从 `pkg/constants/` 原文抄写,无遗漏
5.检查是否禁止使用行内注释替代 description 5.description 格式统一(冒号 `:`,括号,逗号)
6.检查枚举值是否使用中文而非英文 6.Response DTO 有 `_name` 伴生字段
7.重新生成 OpenAPI 文档验证:`go run cmd/gendocs/main.go` 7.validate 标签与 OpenAPI 标签一致
8. ✅ 重新生成 OpenAPI 文档验证:`go run cmd/gendocs/main.go`
**详细检查清单**: 参见 `docs/code-review-checklist.md` **完整枚举规范**: 参见 [`docs/enum-status-standards.md`](../../docs/enum-status-standards.md)
---
## 常见枚举字段标准值 ## 常见枚举字段标准值
```go ```go
// 用户类型 // 用户类型(从 constants.UserType* 抄)
description:"用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)" description:"用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)"
// 角色类型 // 通用启用/禁用(从 constants.StatusEnabled/Disabled 抄)
description:"角色类型 (1:平台角色, 2:客户角色)" description:"状态 (0:禁用, 1:启用)"
// 充值状态(从 constants.RechargeStatus* 抄)
description:"状态 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款)"
// 权限类型 // 权限类型
description:"权限类型 (1:菜单, 2:按钮)" description:"权限类型 (1:菜单, 2:按钮)"
@@ -141,9 +241,6 @@ description:"权限类型 (1:菜单, 2:按钮)"
// 适用端口 // 适用端口
description:"适用端口 (all:全部, web:Web后台, h5:H5端)" description:"适用端口 (all:全部, web:Web后台, h5:H5端)"
// 状态
description:"状态 (0:禁用, 1:启用)"
// 店铺层级 // 店铺层级
description:"店铺层级 (1-7级)" description:"店铺层级 (1-7级)"
``` ```

1
.claude/skills/grill-me Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/grill-me

View File

@@ -0,0 +1 @@
../../.agents/skills/grill-with-docs

1
.claude/skills/grilling Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/grilling

1
.claude/skills/handoff Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/handoff

View File

@@ -0,0 +1,777 @@
---
name: hurl-test
description: Hurl 接口测试生成器。用户描述要测试的接口或业务流程,自动探索代码、确认需求、生成完整的 .hurl 测试文件(含 DTO 驱动的字段完整性断言。触发词测试、hurl、写测试、接口测试。
---
# Hurl 接口测试生成器
**用户描述要测试什么,你来读代码、问确认、生成 .hurl 文件。**
适用于任何后端项目Go / Python / Node / Java 等),不预设框架和目录结构。
---
## 触发条件
以下情况必须使用本 Skill
- 用户说"测试 XX 接口"、"写 hurl 测试"、"给 XX 加测试"
- 用户说"测试 XX 流程"、"测试 XX 的业务逻辑"
- 用户说"验证 XX 接口的字段"、"测试接口契约"
- 用户提到 hurl、.hurl、接口测试、集成测试、冒烟测试
---
## 四阶段工作流(必须按顺序执行)
```
Phase 1: 探索 → Phase 2: 确认 → Phase 3: 生成 → Phase 4: 验证
读代码搞清楚 展示给用户确认 输出 .hurl 文件 语法检查 + 试跑
```
---
### Phase 1: 探索Explore
**目标:读代码,搞清楚项目约定 + 涉及的接口 + 字段 + 依赖。**
#### 1.1 项目画像(首次使用时必须执行,后续复用)
首次为项目生成 Hurl 测试时,先回答以下问题(通过读代码,不要猜):
| 问题 | 怎么找 |
|------|--------|
| **语言/框架** | 看 go.mod / package.json / requirements.txt / pom.xml |
| **路由注册在哪** | 搜索 `router``app.Get``@GetMapping``@app.route` 等关键词 |
| **请求/响应 schema 定义在哪** | 搜索 DTO / schema / serializer / model 目录,看 json tag 或装饰器 |
| **统一响应格式是什么** | 找 response helper 文件(如 `response.go``response.py`),记录 JSON 结构 |
| **认证方式是什么** | 找 auth middleware确定是 Bearer Token / Cookie / API Key / Basic Auth |
| **登录接口是什么** | 找登录 handler记录路径、请求体、响应中 token 的位置 |
| **分页格式是什么** | 找列表接口的响应结构,记录 items/total/page 等字段名 |
| **已有 hurl 测试吗** | 搜索 `*.hurl` 文件,复用已有的约定 |
将画像结果**写入 `tests/hurl/.project-profile.md` 文件持久化保存**。
#### 画像持久化(关键机制)
**首次使用时**:完成 1.1 探索后,将画像写入 `tests/hurl/.project-profile.md`,格式如下:
```markdown
# 项目画像Hurl 测试自动生成用)
<!-- 由 hurl-test skill 自动生成,请勿手动修改 -->
<!-- 如需刷新,删除此文件后重新运行 skill -->
## 技术栈
- 语言: Go 1.25
- 框架: Fiber v2
- ORM: GORM
## 路由定义位置
- 路由注册入口: internal/routes/routes.go
- 按模块拆分: internal/routes/{module}.go
- 路由注册函数: Register(router, doc, basePath, method, path, handler, spec)
## Schema 定义位置
- DTO 目录: internal/model/dto/
- 命名规则: {module}_dto.go
- 字段标签: json / validate / description
## 统一响应格式
{code: int, msg: string, data: any, timestamp: string(RFC3339)}
- 成功: code=0, msg="success"
- 错误: code!=0
## 分页格式
{items: [], total: int, page: int, size: int}
- 包裹在 data 字段内: $.data.items / $.data.total
## 认证方式
- 后台: POST /api/auth/admin-login → $.data.access_token → Authorization: Bearer {token}
- C端: JWT → Authorization: Bearer {token}
## 默认测试账号
- 用户名: admin
- 密码: Admin@123456
## 服务端口
- 默认: 3000
```
**后续使用时**:检查 `tests/hurl/.project-profile.md` 是否存在:
- **存在** → 直接读取,跳过 1.1 的探索步骤,节省时间
- **不存在** → 执行 1.1 完整探索,然后生成此文件
- **用户说"刷新画像"** → 删除旧文件,重新执行 1.1
#### 1.2 找接口定义
根据用户要测的模块,定位路由注册代码,提取:
- **HTTP 方法**GET / POST / PUT / DELETE / PATCH
- **路由路径**(含路径参数格式,如 `/users/:id``/users/{id}`
- **接口说明**注释、Summary、装饰器描述
- **是否需要认证**
- **请求 schema 类型名**Input / Request DTO
- **响应 schema 类型名**Output / Response DTO
#### 1.3 读 schema 定义DTO / struct / class / type
定位请求和响应的 schema 定义文件,提取每个字段的:
- **字段名**JSON 序列化后的名称json tag / @JsonProperty / serializer field
- **语言类型**string / int / bool / 数组 / 嵌套对象 / 可空等
- **是否必填**validate tag / required 装饰器 / 非空标注
- **是否可空**:指针类型 / Optional / nullable
- **是否参与序列化**`json:"-"` / @JsonIgnore / exclude
- **是否 omitempty**`json:",omitempty"` / 条件序列化
- **字段描述**description tag / docstring / 注释
#### 1.4 识别业务依赖
读 service / business logic 层,识别:
- 创建操作需要哪些前置数据(如创建订单需要先有商品和用户)
- 是否有唯一性约束(如用户名不能重复)
- 是否依赖外部服务支付网关、短信、OAuth 等)
- 业务流转逻辑(状态机、级联操作)
---
### Phase 2: 确认Clarify
**目标:向用户展示发现的内容,确认模糊点。不要闷头生成。**
#### 2.1 必须展示的内容
```
我梳理了相关代码,发现以下信息:
📋 涉及接口:
- [方法] [路径] - [说明](认证: 是/否)
- ...
📦 响应字段(基于 {SchemaName}
- [字段名]: [类型] - [说明]
- ...(共 N 个字段,将全部生成断言)
🔗 依赖关系:
- [创建 X 需要先创建 Y]
- ...
⚠️ 特殊情况:
- [涉及外部服务 / 文件上传 / 特殊认证等]
```
#### 2.2 按需确认(只问有歧义的)
| 场景 | 要问的 |
|------|--------|
| 流程范围不明确 | "要测到哪一步?" |
| 多种用户角色 | "用哪种身份测?" |
| 是否测异常 | "需要包含异常 case 吗?(参数校验失败、权限不足等)" |
| 是否测数据隔离 | "需要验证不同用户间数据不可见吗?" |
| 涉及第三方 | "XX 部分怎么处理?绕过 / 模拟回调 / 跳过?" |
| 前置数据来源 | "XX 依赖数据是通过 API 创建还是假设已存在?" |
**如果用户说"越完整越好"或"都要"→ 默认全部包含,不再追问。**
---
### Phase 3: 生成Generate
**目标:生成完整的 .hurl 文件,字段断言基于 schema 代码,不能编造。**
#### 3.1 文件头注释
```hurl
# ============================================================
# 测试:{测试名称}
# 生成时间:{日期}
# 涉及模块:{module1, module2, ...}
# 涉及接口:{N} 个
# 断言数量:{N} 条
# 前置条件:{服务运行 + 必要的前置条件}
# ============================================================
# 流程:
# 1. {步骤描述}
# 2. {步骤描述}
# ...
# ============================================================
```
#### 3.2 请求生成规则
**认证**
- 根据 Phase 1 画像中的登录接口和 token 位置生成
- token 必须通过 `[Captures]` 捕获,后续请求引用
- 如果是 Cookie 认证,用 `[Cookies]` 或 cookie capture
**CRUD 标准模式**
| 操作 | 生成要求 |
|------|---------|
| **创建POST** | capture 返回的 ID唯一字段用 `{{newUuid}}` 防冲突 |
| **查询详情GET** | **逐字段断言**(类型 + 值,见 3.3 |
| **查询列表GET** | 分页结构断言 + items[0] 逐字段断言 |
| **修改PUT/PATCH** | 修改后**紧跟一个 GET 验证修改生效** |
| **删除DELETE** | 删除后**紧跟一个 GET 验证已删除** |
**业务流程模式**
- 按用户描述的流程顺序编排请求
- 上一步的输出ID、状态等通过 `[Captures]` 传给下一步
- 关键步骤加中间状态验证(如创建订单后验证状态为"待支付"
#### 3.3 schema 到断言的映射
读到 schema 字段后,按以下规则生成 jsonpath 断言:
**通用类型映射(所有语言)**
| Schema 类型特征 | Hurl 断言 |
|----------------|-----------|
| 字符串string / str / String | `isString` |
| 整数int / integer / long / Int | `isInteger` |
| 浮点float / double / decimal / Float | `isNumber` |
| 布尔bool / boolean / Boolean | `isBoolean` |
| 数组 / 列表([] / List / Array | `isList` |
| 嵌套对象struct / class / dict / object | `isObject`,并递归检查子字段 |
| 可空类型(指针 / Optional / nullable | `exists`(不强制类型,因为可能是 null |
| 不参与序列化json:"-" / @JsonIgnore / exclude=True | **跳过,不生成断言** |
| 条件序列化omitempty / if not None | `exists` 或不生成(取决于场景) |
**Go 特定映射**
| Go 类型 | Hurl 断言 |
|---------|-----------|
| `string` | `isString` |
| `int`, `int8/16/32/64`, `uint`, `uint8/16/32/64` | `isInteger` |
| `float32`, `float64` | `isNumber` |
| `bool` | `isBoolean` |
| `[]T` | `isList` |
| `*string`, `*int`, `*uint` 等指针 | `exists` |
| `time.Time` | `isString`(通常序列化为字符串) |
| `map[string]any` | `isObject` |
**Python 特定映射Pydantic / Django / FastAPI**
| Python 类型 | Hurl 断言 |
|------------|-----------|
| `str` | `isString` |
| `int` | `isInteger` |
| `float`, `Decimal` | `isNumber` |
| `bool` | `isBoolean` |
| `list[T]`, `List[T]` | `isList` |
| `Optional[T]`, `T | None` | `exists` |
| `dict`, `Dict` | `isObject` |
| `datetime`, `date` | `isString` |
**TypeScript/JavaScript 特定映射**
| TS/JS 类型 | Hurl 断言 |
|-----------|-----------|
| `string` | `isString` |
| `number`(整数上下文) | `isInteger` |
| `number`(通用) | `isNumber` |
| `boolean` | `isBoolean` |
| `T[]`, `Array<T>` | `isList` |
| `T \| null`, `T \| undefined` | `exists` |
| `object`, `Record<>` | `isObject` |
| `Date` | `isString` |
**Java 特定映射**
| Java 类型 | Hurl 断言 |
|----------|-----------|
| `String` | `isString` |
| `Integer`, `Long`, `int`, `long` | `isInteger` |
| `Double`, `Float`, `BigDecimal` | `isNumber` |
| `Boolean`, `boolean` | `isBoolean` |
| `List<T>` | `isList` |
| `@Nullable`, `Optional<T>` | `exists` |
| `Map<K,V>` | `isObject` |
| `LocalDateTime`, `Instant` | `isString` |
#### 3.4 统一响应格式断言
根据 Phase 1 画像中发现的统一响应格式,为**每个成功响应**添加格式断言。
示例:如果项目的统一格式是 `{code, msg, data, timestamp}`
```hurl
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.msg" == "success"
jsonpath "$.timestamp" isIsoDate
```
示例:如果项目的格式是 `{status, message, result}`
```hurl
[Asserts]
jsonpath "$.status" == "ok"
jsonpath "$.message" isString
```
示例:如果项目无统一包装,直接返回数据:
```hurl
[Asserts]
# 直接断言业务字段
jsonpath "$.id" isInteger
jsonpath "$.name" isString
```
**不要假设响应格式,必须从代码中确认。**
#### 3.5 分页断言
根据 Phase 1 画像中发现的分页结构生成。
示例:如果是 `{items, total, page, size}` 格式:
```hurl
jsonpath "$.data.items" isList
jsonpath "$.data.total" isInteger
jsonpath "$.data.total" >= 1
jsonpath "$.data.page" isInteger
jsonpath "$.data.size" isInteger
# items 内元素逐字段断言
jsonpath "$.data.items[0].{field}" {type_assert}
```
示例:如果是 `{results, count, next, previous}` 格式Django 风格):
```hurl
jsonpath "$.results" isList
jsonpath "$.count" isInteger
jsonpath "$.count" >= 1
# results 内元素逐字段断言
jsonpath "$.results[0].{field}" {type_assert}
```
**根据实际代码调整字段名,不硬编码。**
#### 3.6 异常 Case 模板
**参数校验失败**
```hurl
# ── 异常:参数校验失败 ──
POST {{base_url}}/{path}
Authorization: Bearer {{token}}
Content-Type: application/json
{
"required_field": ""
}
HTTP {expected_error_status}
[Asserts]
# 断言错误响应格式(根据项目约定调整)
```
> HTTP 状态码根据项目实际返回确定:有的项目错误也返回 200 + 业务错误码,有的返回 400/422。
**未认证访问**
```hurl
# ── 异常:未认证访问 ──
GET {{base_url}}/{protected_path}
HTTP {expected_unauth_status}
```
**越权访问**(如果用户要求):
```hurl
# ── 异常:用户 B 不能访问用户 A 的资源 ──
GET {{base_url}}/{path}/{{user_a_resource_id}}
Authorization: Bearer {{user_b_token}}
HTTP {expected_forbidden_status}
```
#### 3.7 特殊场景处理
| 场景 | 处理策略 |
|------|---------|
| **短信/邮件验证码** | 建议服务端加 test_mode 开关,固定验证码写入 env 文件;注释提醒用户 |
| **第三方支付** | 优先用项目内部支付方式(如钱包支付);如需测回调,直接 POST 回调接口模拟 |
| **OAuth 登录(微信/Google/GitHub** | 建议服务端加 test_mode 支持直接传 openid/email注释提醒用户 |
| **文件上传** | 用 Hurl 的 `[Multipart]` 语法 + testdata 目录下的样本文件 |
| **外部 API 依赖** | 只测参数校验和错误响应格式,不断言业务结果;注释说明依赖 |
| **WebSocket** | Hurl 不支持,注释说明跳过 |
| **异步任务结果** | 用 Hurl 的 `retry` + `retry-interval` 轮询直到状态变更 |
异步轮询示例:
```hurl
# 等待异步任务完成(最多重试 10 次,间隔 500ms
GET {{base_url}}/{path}/{{task_id}}
Authorization: Bearer {{token}}
[Options]
retry: 10
retry-interval: 500ms
HTTP 200
[Asserts]
jsonpath "$.data.status" == "completed"
```
#### 3.8 文件输出
**目录结构**(首次使用时创建,如不存在):
```
tests/hurl/
├── env/
│ └── dev.env # 环境变量
├── testdata/ # 测试用的样本文件
├── flows/ # 业务流程测试
├── modules/ # 按模块的接口测试
│ └── {module}/
│ └── 01-crud.hurl
├── negative/ # 异常/边界测试
├── contract/ # 接口契约验证
└── Makefile # 快捷命令
```
文件放置规则:
| 用户描述 | 输出路径 |
|---------|---------|
| 测试 XX 流程 / 业务流程 | `tests/hurl/flows/{flow-name}.hurl` |
| 测试 XX 模块的 CRUD / 接口 | `tests/hurl/modules/{module}/01-crud.hurl` |
| 测试异常/边界/权限 | `tests/hurl/negative/{name}.hurl` |
| 测试接口契约/字段对齐 | `tests/hurl/contract/{name}.hurl` |
**如果项目已有 hurl 测试目录结构,沿用已有约定,不要另起炉灶。**
#### 3.9 env 和 Makefile
**env/dev.env**(首次创建时生成,内容基于 Phase 1 画像):
```properties
# 服务地址
base_url=http://localhost:{port}
# 认证信息(根据项目实际填写)
admin_username={默认用户名}
admin_password={默认密码}
# 测试模式变量(如果有特殊场景)
# test_sms_code=888888
# test_openid=test_openid_001
```
**Makefile**(首次创建时生成):
```makefile
SHELL := /bin/bash
ENV ?= dev
HURL_OPTS := --variables-file env/$(ENV).env --test
.PHONY: test test-flows test-modules test-negative report
test: ## 运行所有测试
hurl $(HURL_OPTS) .
test-flows: ## 运行业务流程测试
hurl $(HURL_OPTS) flows/
test-modules: ## 运行模块接口测试
hurl $(HURL_OPTS) modules/
test-negative: ## 运行异常测试
hurl $(HURL_OPTS) negative/
report: ## 生成 HTML 报告
hurl $(HURL_OPTS) --report-html build/report/ .
```
---
### Phase 4: 验证Verify
**目标:确保生成的 .hurl 文件语法正确、可运行。**
#### 4.1 语法自检
- [ ] 每个请求之间有空行分隔
- [ ] `[Captures]``[Asserts]` 拼写正确(大小写敏感)
- [ ] 所有 `{{变量}}` 引用都有来源env 文件定义 或 上游 `[Captures]`
- [ ] JSON body 无尾逗号、格式正确
- [ ] `HTTP {status}` 在请求之后、`[Captures]` / `[Asserts]` 之前
- [ ] 请求和断言之间没有多余空行(`HTTP` 行必须紧跟请求)
- [ ] 文件上传路径相对于 hurl 文件位置正确
#### 4.2 运行测试
```bash
hurl --variables-file tests/hurl/env/dev.env --test tests/hurl/{生成的文件}
```
- 服务在跑 → 执行,如有失败分析修正
- 服务没跑 → 跳过,告知用户手动验证命令
---
## 红线规则
| 规则 | 说明 |
|------|------|
| **不跳过 Phase 1** | 必须读代码确认接口路径和字段,不能凭记忆或猜测 |
| **不跳过 Phase 2** | 必须向用户展示发现的接口和字段,确认后再生成 |
| **不编造字段** | 所有断言的字段名必须来自实际 schema 代码 |
| **不编造路径** | 所有接口路径必须来自实际路由代码 |
| **不遗漏字段** | schema 中每个参与序列化的字段都必须有对应断言 |
| **不硬编码 ID** | 所有依赖的 ID 通过 `[Captures]` 从上游请求获取 |
| **不假设响应格式** | 统一响应结构必须从代码中确认,不同项目格式不同 |
| **唯一值防冲突** | 创建类请求的唯一字段使用 `{{newUuid}}``{{newDate}}` |
| **自给自足** | 每个 .hurl 文件自己创建测试数据,不依赖外部数据准备 |
---
## AI 助手检查清单
生成 .hurl 文件后自检:
1. ✅ 文件头注释包含流程描述和前置条件
2. ✅ 认证步骤正确 capture 了 token / cookie
3. ✅ 所有依赖数据通过 API 链式创建(自给自足)
4. ✅ 每个成功响应断言了项目的统一响应格式
5. ✅ 查询详情接口**逐字段断言**(类型 + 值,基于 schema
6. ✅ 分页接口断言了分页结构 + 第一条记录的字段
7. ✅ 修改操作后紧跟 GET 验证修改生效
8. ✅ 删除操作后紧跟 GET 验证已删除
9. ✅ 唯一字段使用了 `{{newUuid}}`
10.`{{变量}}` 引用无悬空(都有 env 或 capture 来源)
11. ✅ 文件放在了正确的目录位置
12. ✅ 特殊场景有明确的处理策略和注释提醒
---
## 附录Hurl 语法速查
**生成 .hurl 文件时必须参照本速查,不可凭记忆编造语法。**
### 文件结构
一个 .hurl 文件由多个 entry 组成,每个 entry = 请求 + 可选响应:
```
请求1
响应1可选
请求2
响应2可选
```
entry 之间用空行分隔。
### 请求格式
```hurl
METHOD URL
Header1: value1
Header2: value2
[Options]
key: value
[Query]
param1: value1
[Form]
field1: value1
[Multipart]
file1: file,path/to/file;
[BasicAuth]
username: password
[Cookies]
name: value
BODYJSON / XML / multiline string / file
```
**规则**
- Method + URL 是第一行,必须
- Headers 紧跟 URL 之后(无 section 标记)
- Sections`[Query]``[Form]``[Options]` 等)顺序任意
- Body 必须在最后
- JSON body 直接写 `{ }` 即可,自动设置 Content-Type: application/json
### 响应格式
```hurl
HTTP {status_code}
Header1: expected_value1
[Captures]
var_name: jsonpath "$.path"
[Asserts]
jsonpath "$.field" == "value"
```
**规则**
- `HTTP {status}` 紧跟请求之后(中间不能有空行)
- `HTTP *` 表示不检查状态码
- Headers 检查紧跟 HTTP 行之后
- `[Captures]``[Asserts]` 顺序任意
### 变量和模板
```hurl
# 引用变量(从 env 文件、命令行或上游 capture 获取)
GET {{base_url}}/api/users/{{user_id}}
# 内置函数
POST {{base_url}}/api/users
{
"email": "{{newUuid}}@test.com",
"created_at": "{{newDate}}"
}
```
可用函数:
- `{{newUuid}}` — 生成 UUID v4
- `{{newDate}}` — 生成 RFC 3339 UTC 时间戳
### Capture 语法
```hurl
[Captures]
# JSONPath
token: jsonpath "$.data.access_token"
user_id: jsonpath "$.data.id"
first_item: jsonpath "$.items[0].name"
# Header
location: header "Location"
# Cookie
session: cookie "SESSIONID"
# Body整个响应体作为字符串
full_body: body
# Status code
code: status
# 正则表达式
csrf: regex "name=\"csrf\" value=\"([^\"]+)\""
# 响应时间(毫秒)
response_time: duration
```
### Assert 语法
```hurl
[Asserts]
# ── 状态码 ──
status == 200
status >= 200
status < 300
# ── JSONPath 断言 ──
jsonpath "$.name" == "Alice" # 等于
jsonpath "$.name" != "Bob" # 不等于
jsonpath "$.age" > 18 # 大于
jsonpath "$.age" >= 18 # 大于等于
jsonpath "$.count" < 100 # 小于
jsonpath "$.items" count == 5 # 集合长度
jsonpath "$.name" startsWith "Al" # 前缀
jsonpath "$.name" endsWith "ce" # 后缀
jsonpath "$.name" contains "lic" # 包含
jsonpath "$.date" matches /\\d{4}-\\d{2}-\\d{2}/ # 正则
# ── 类型断言 ──
jsonpath "$.name" isString
jsonpath "$.age" isInteger
jsonpath "$.score" isFloat
jsonpath "$.count" isNumber # 整数或浮点
jsonpath "$.active" isBoolean
jsonpath "$.items" isList
jsonpath "$.meta" isObject
jsonpath "$.id" isUuid
jsonpath "$.created_at" isIsoDate # RFC 3339 格式
jsonpath "$.field" isEmpty # 空集合
# ── 存在性 ──
jsonpath "$.field" exists
jsonpath "$.field" not exists
# ── 否定 ──
jsonpath "$.name" not contains "Bob"
jsonpath "$.status" not == "deleted"
# ── Header 断言 ──
header "Content-Type" contains "application/json"
header "X-Request-Id" exists
# ── 性能 ──
duration < 1000 # 响应时间(毫秒)
# ── Body 断言 ──
body contains "Hello"
bytes count == 1024
```
### Options逐请求配置
```hurl
GET {{base_url}}/api/task/{{task_id}}
[Options]
retry: 10 # 最大重试次数(-1 = 无限)
retry-interval: 500ms # 重试间隔
delay: 2s # 请求前等待
location: true # 跟随重定向
insecure: true # 允许不安全 SSL
verbose: true # 输出详细日志
very-verbose: true # 输出更详细日志
skip: true # 跳过此请求
variable: key=value # 定义变量
HTTP 200
```
### Multipart 文件上传
```hurl
POST {{base_url}}/api/upload
[Multipart]
file: file,testdata/sample.xlsx;
field1: value1
# 指定 Content-Type
file2: file,testdata/data.bin; application/octet-stream
```
### 运行命令
```bash
# 运行单个文件
hurl --test file.hurl
# 带变量文件
hurl --variables-file env/dev.env --test file.hurl
# 运行目录下所有 .hurl
hurl --test tests/hurl/
# 生成 HTML 报告
hurl --test --report-html build/report/ tests/hurl/
# 生成 JUnit 报告CI 用)
hurl --test --report-junit build/report.xml tests/hurl/
# 并行执行(--test 默认并行,同文件内串行)
hurl --test --jobs 4 tests/hurl/
# 指定单个变量
hurl --variable base_url=http://localhost:3000 --test file.hurl
# 失败后继续执行
hurl --test --continue-on-error tests/hurl/
```
### 常见错误
| 错误 | 原因 | 修正 |
|------|------|------|
| `HTTP 200` 和请求之间有空行 | 空行会被当作 entry 分隔符 | 删除空行HTTP 行紧跟请求 |
| JSON body 有尾逗号 | Hurl 严格解析 JSON | 删除最后一个逗号 |
| `jsonpath` 写成 `json_path``JsonPath` | 关键字大小写敏感 | 必须小写 `jsonpath` |
| `[Captures]` 写成 `[Capture]` | 必须是复数 | `[Captures]``[Asserts]``[Options]` |
| 变量 `{{ var }}` 有空格 | 允许,但建议统一 | `{{var}}``{{ var }}` 都可以 |
| `isIsoDate` 用在非 RFC 3339 格式 | 只认 `YYYY-MM-DDTHH:mm:ss` 格式 | 如果是其他格式用 `matches` |
| `file,path;` 路径含 `..` | Hurl 禁止相对父目录 | 用 `--file-root` 或调整路径 |

1
.claude/skills/implement Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/implement

View File

@@ -0,0 +1 @@
../../.agents/skills/improve-codebase-architecture

View File

@@ -0,0 +1,97 @@
---
name: openspec-api-contract
description: OpenSpec API 契约规范。创建涉及接口的 OpenSpec 提案时使用。探索阶段提供业务与契约引导清单,提案文档要求 API 契约设计、错误码与完成标准等必填章节。
---
# OpenSpec API 契约规范
**适用场景**:创建涉及 API/接口的 OpenSpec 提案时,探索和提案两个阶段均须遵守本规范。
---
## 一、探索阶段引导清单
`openspec-explore` 阶段,当内容涉及接口时,讨论必须覆盖以下所有维度。
### 业务与契约确认
**输入与输出**
- 请求方是谁用户类型SuperAdmin/Platform/Agent/Enterprise/Personal
- 输入参数:必填/选填字段、格式约束、参数来源(路径/查询/Body
- 输出结构:哪些字段必须返回?是否需要分页?
**业务规则**
- 核心业务规则与边界条件?
- 是否涉及状态流转?状态机的完整定义?
- 依赖外部服务时,外部异常的降级行为?
**权限与资源所有权**
- 哪些用户类型可以访问?
- 是否涉及跨用户/跨店铺/跨企业的资源访问?(需三层越权防护)
- 资源所有权校验方式:`CanManageShop` / `CanManageEnterprise` / 自有资源?
**幂等性**
- 操作类型:查询(天然幂等)/ 创建 / 更新 / 删除?
- 写操作幂等策略:状态条件更新 / Redis 业务键防重 + 分布式锁 / 乐观锁version
- 异步任务是否需要任务锁?
**数据模型变更**
- 是否需要新建表、修改现有表或数据回填?
- 迁移策略:上线顺序、兼容旧数据的方式?
- 是否影响 GORM Callback 自动数据权限过滤?
**错误码与异常语义**
- 预期错误场景及对应错误码?
- 错误响应是否泄露敏感信息?(参数校验失败统一返回 `CodeInvalidParam`
---
## 二、提案文档必填章节
`openspec-propose` 生成的提案(`proposal.md` / `design.md`)中,涉及接口时以下内容**不可缺失**。
### API 契约设计
**接口定义**
| 项 | 内容 |
|---|---|
| Endpoint | `METHOD /api/{scope}/{resource}[/:id]` |
| 请求参数 | 字段名、类型、必填/选填、说明 |
| 响应结构 | `data` 字段的完整结构定义 |
| 鉴权要求 | 允许的用户类型 |
| 资源所有权 | 所有权校验方式 |
**列表接口额外要求**
- 分页:`page` + `page_size`(默认 20最大 100
- 排序:默认排序字段与方向
- 过滤:支持的过滤条件列表
**错误码清单**
| 场景 | 错误码 | 说明 |
|---|---|---|
| 参数校验失败 | `CodeInvalidParam` | 统一返回,不泄露细节 |
| 资源不存在/越权 | `CodeForbidden` | 不区分两者,防止信息泄露 |
| (业务错误场景... | (对应错误码) | (说明) |
### 完成标准
**最小验证步骤**(按顺序列出可操作的验证步骤)
1. (例:调用创建接口,验证返回 `code=0`
2. (例:查询接口确认数据存在且字段正确)
3. PostgreSQL MCP 查询确认数据库记录符合预期)
**影响范围说明**
- 新增/修改的表:
- 影响的现有接口:
- 影响的权限与数据过滤范围:
---
## 约束(必须遵守)
- **优先复用现有架构与库**:不引入新依赖,错误码优先复用已有定义
- **不做顺手重构**:提案范围严格限定在目标功能;发现可优化点,记录到 backlog 但不执行
- **数据库设计**:禁止外键约束,禁止 GORM 关联标签,关联通过 ID 字段手动维护

1
.claude/skills/prototype Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/prototype

1
.claude/skills/research Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/research

View File

@@ -0,0 +1 @@
../../.agents/skills/resolving-merge-conflicts

View File

@@ -0,0 +1 @@
../../.agents/skills/setup-matt-pocock-skills

View File

@@ -1,260 +0,0 @@
---
name: systematic-debugging
description: 遇到任何 bug、异常行为、报错时必须使用。在提出任何修复方案之前强制执行根因分析流程。适用于 API 报错、数据异常、业务逻辑错误、性能问题等所有技术问题。
---
# 系统化调试方法论
## 铁律
```
没有找到根因,禁止提出任何修复方案。
```
改之前先搞懂为什么坏了。猜测不是调试,验证假设才是。
---
## 什么时候用
**所有技术问题都用这个流程**
- API 接口报错4xx / 5xx
- 业务数据异常(金额不对、状态流转错误)
- 性能问题(接口慢、数据库慢查询)
- 异步任务失败Asynq 任务报错/卡住)
- 构建失败、启动失败
**尤其是以下场景**
- 时间紧迫(越急越不能瞎猜)
- "很简单的问题"(简单问题也有根因)
- 已经试了一次修复但没解决
- 不完全理解为什么出问题
---
## 四阶段流程
必须按顺序完成每个阶段,不可跳过。
### 阶段一:根因调查
**这是最重要的阶段,占整个调试时间的 60%。没完成本阶段,禁止进入阶段二。**
#### 1. 仔细阅读错误信息
- 完整阅读 stack trace不要跳过
- 注意行号、文件路径、错误码
- 很多时候答案就在错误信息里
- 检查 `logs/app.log``logs/access.log` 中的上下文
#### 2. 稳定复现
- 能稳定触发吗?精确的请求参数是什么?
- 用 curl 或 Postman 复现,记录完整的请求和响应
- 不能复现 → 收集更多数据检查日志、Redis 状态、数据库记录),**不要瞎猜**
#### 3. 检查最近改动
- `git diff` / `git log --oneline -10` 看最近改了什么
- 新加了什么依赖?改了什么配置?改了什么 SQL
- 对比改动前后的行为差异
#### 4. 逐层诊断(针对本项目架构)
本项目有明确的分层架构,问题一定出在某一层的边界:
```
请求 → Fiber Middleware → Handler → Service → Store → PostgreSQL/Redis
↑ ↑ ↑ ↑ ↑
认证/限流 参数解析 业务逻辑 SQL/缓存 数据本身
```
**在每个层边界确认数据是否正确**
```go
// Handler 层 — 请求进来的参数对不对?
logger.Info("Handler 收到请求",
zap.Any("params", req),
zap.String("request_id", requestID),
)
// Service 层 — 传给业务逻辑的数据对不对?
logger.Info("Service 开始处理",
zap.Uint("user_id", userID),
zap.Any("input", input),
)
// Store 层 — SQL 查询/写入的数据对不对?
// 开启 GORM Debug 模式查看实际 SQL
db.Debug().Where(...).Find(&result)
// Redis 层 — 缓存的数据对不对?
// 用 redis-cli 直接检查 key 的值
// GET auth:token:{token}
// GET sim:status:{iccid}
```
**跑一次 → 看日志 → 找到断裂的那一层 → 再深入该层排查。**
#### 5. 追踪数据流
如果错误深藏在调用链中:
- 坏数据从哪来的?
- 谁调用了这个函数,传了什么参数?
- 一直往上追,直到找到数据变坏的源头
- **修源头,不修症状**
---
### 阶段二:模式分析
**找到参照物,对比差异。**
#### 1. 找能用的参照
项目里有没有类似的、能正常工作的代码?
| 如果问题在... | 参照物在... |
|-------------|-----------|
| Handler 参数解析 | 其他 Handler 的相同模式 |
| Service 业务逻辑 | 同模块其他方法的实现 |
| Store SQL 查询 | 同 Store 文件中类似的查询 |
| Redis 操作 | `pkg/constants/redis.go` 中的 Key 定义 |
| 异步任务 | `internal/task/` 中其他任务处理器 |
| GORM Callback | `pkg/database/` 中的 callback 实现 |
#### 2. 逐行对比
完整阅读参考代码,不要跳读。列出每一处差异。
#### 3. 不要假设"这个不重要"
小差异经常是 bug 的根因:
- 字段标签 `gorm:"column:xxx"` 拼写不对
- `errors.New()` 用了错误的错误码
- Redis Key 函数参数传反了
- Context 里的 UserID 没取到(中间件没配)
---
### 阶段三:假设和验证
**科学方法:一次只验证一个假设。**
#### 1. 形成单一假设
明确写下:
> "我认为根因是 X因为 Y。验证方法是 Z。"
#### 2. 最小化验证
- 只改一个地方
- 一次只验证一个变量
- 不要同时修多处
#### 3. 验证结果
- 假设成立 → 进入阶段四
- 假设不成立 → 回到阶段一,用新信息重新分析
- **绝对不能在失败的修复上再叠加修复**
#### 4. 三次失败 → 停下来
如果连续 3 次假设都不成立:
**这不是 bug是架构问题。**
- 停止一切修复尝试
- 整理已知信息
- 向用户说明情况,讨论是否需要重构
- 不要再试第 4 次
---
### 阶段四:实施修复
**确认根因后,一次性修好。**
#### 1. 修根因,不修症状
```
❌ 症状修复:在 Handler 里加个 if 把坏数据过滤掉
✅ 根因修复:修 Service 层生成坏数据的逻辑
```
#### 2. 一次只改一个地方
- 不搞"顺手优化"
- 不在修 bug 的同时重构代码
- 修完 bug 就停
#### 3. 验证修复
- `go build ./...` 编译通过
- `lsp_diagnostics` 无新增错误
- 用原来复现 bug 的请求再跑一次,确认修好了
- 用 PostgreSQL MCP 工具检查数据库中的数据状态
#### 4. 清理诊断代码
- 删除阶段一加的临时诊断日志(除非它们本身就该保留)
- 确保没有 `db.Debug()` 残留在代码里
---
## 本项目常见调试场景速查
| 场景 | 首先检查 |
|------|---------|
| API 返回 401 | `logs/access.log` 中该请求的 token → Redis 中 `auth:token:{token}` 是否存在 |
| API 返回 403 | 用户类型是什么 → GORM Callback 自动过滤的条件对不对 → `middleware.CanManageShop()` 的参数 |
| 数据查不到 | GORM 数据权限过滤有没有生效 → `shop_id` / `enterprise_id` 是否正确 → 是否需要 `SkipDataPermission` |
| 金额/余额不对 | 乐观锁 version 字段 → `RowsAffected` 是否为 0 → 并发场景下的锁竞争 |
| 状态流转错误 | `WHERE status = expected` 条件更新 → 状态机是否有遗漏的路径 |
| 异步任务不执行 | Asynq Dashboard → `RedisTaskLockKey` 有没有残留 → Worker 日志 |
| 异步任务重复执行 | `RedisTaskLockKey` 的 TTL → 任务幂等性检查 |
| 分佣计算错误 | 佣金类型(差价/一次性) → 套餐级别的佣金率 → 设备级防重复分佣 |
| 套餐激活异常 | 卡状态 → 实名状态 → 主套餐排队逻辑 → 加油包绑定关系 |
| Redis 缓存不一致 | Key 的 TTL → 缓存更新时机 → 是否有手动 `Del` 清除 |
| 微信支付回调失败 | 签名验证 → 幂等性处理 → 回调 URL 是否可达 |
| GORM 查询慢 | `db.Debug()` 看实际 SQL → 是否 N+1 → 是否缺少索引 |
---
## 红线规则
如果你发现自己在想以下任何一条,**立刻停下来,回到阶段一**
| 想法 | 为什么是错的 |
|------|------------|
| "先快速修一下,回头再查" | 快速修 = 猜测。猜测 = 浪费时间。 |
| "试试改这个看看行不行" | 一次只验证一个假设,不是随机改。 |
| "大概是 X 的问题,我直接改了" | "大概"不是根因。先验证再改。 |
| "这个很简单,不用走流程" | 简单问题走流程只需要 5 分钟。不走流程可能浪费 2 小时。 |
| "我不完全理解但这应该行" | 不理解 = 没找到根因。回阶段一。 |
| "再试一次"(已经失败 2 次) | 3 次失败 = 架构问题。停下来讨论。 |
| "同时改这几个地方应该能修好" | 改多处 = 无法确认哪个是根因。一次只改一处。 |
---
## 常见借口和真相
| 借口 | 真相 |
|------|------|
| "问题很简单,不需要走流程" | 简单问题也有根因。走流程对简单问题只花 5 分钟。 |
| "太紧急了,没时间分析" | 系统化调试比乱猜快 3-5 倍。越急越要走流程。 |
| "先改了验证一下" | 这叫猜测,不叫验证。先确认根因再改。 |
| "我看到问题了,直接修" | 看到症状 ≠ 理解根因。症状修复是技术债。 |
| "改了好几个地方,反正能用了" | 不知道哪个改动修的,下次还会出问题。 |
---
## 快速参考
| 阶段 | 核心动作 | 完成标准 |
|------|---------|---------|
| **一、根因调查** | 读错误日志、复现、检查改动、逐层诊断、追踪数据流 | 能说清楚"因为 X 所以 Y" |
| **二、模式分析** | 找参照代码、逐行对比、列出差异 | 知道正确的应该长什么样 |
| **三、假设验证** | 写下假设、最小改动、单变量验证 | 假设被证实或推翻 |
| **四、实施修复** | 修根因、编译检查、请求验证、清理诊断代码 | bug 消失,无新增问题 |

1
.claude/skills/tdd Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/tdd

1
.claude/skills/teach Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/teach

1
.claude/skills/to-issues Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/to-issues

1
.claude/skills/to-prd Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/to-prd

Some files were not shown because too many files have changed in this diff Show More