From c6ef6820e4b7ec30a6d13a79e56639aefab220f2 Mon Sep 17 00:00:00 2001 From: johnQ Date: Sun, 1 Nov 2026 05:50:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(governance):=20=E8=8A=82=E6=8B=8D=E8=90=BD?= =?UTF-8?q?=E5=88=B0=20launchd=20+=20=E6=8F=90=E6=A1=88=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/scheduled-task.sh:daily/weekly/monthly 只做确定性动作,需模型判断的部分生成 queue 文件 - scripts/install-schedulers.sh:装成用户级 launchd(daily 07:20 / weekly 周一 10:00 / monthly 1 日 10:00) - .gitea issue/PR 模板:新技能提案走同一套 SCOPE 门槛问题 - README 一键部署 2 步 → 4 步;MAINTENANCE 增加定时任务一节与边界说明 --- .gitea/issue_template/config.yaml | 2 + .gitea/issue_template/new-skill.md | 36 +++++++ .gitea/pull_request_template.md | 24 +++++ .gitignore | 6 ++ CHANGELOG.md | 8 ++ MAINTENANCE.md | 21 ++++ README.md | 10 +- scripts/install-schedulers.sh | 143 +++++++++++++++++++++++++ scripts/scheduled-task.sh | 162 +++++++++++++++++++++++++++++ 9 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 .gitea/issue_template/config.yaml create mode 100644 .gitea/issue_template/new-skill.md create mode 100644 .gitea/pull_request_template.md create mode 100755 scripts/install-schedulers.sh create mode 100755 scripts/scheduled-task.sh diff --git a/.gitea/issue_template/config.yaml b/.gitea/issue_template/config.yaml new file mode 100644 index 0000000..64eb98d --- /dev/null +++ b/.gitea/issue_template/config.yaml @@ -0,0 +1,2 @@ +blank_issues_enabled: true +contact_links: [] diff --git a/.gitea/issue_template/new-skill.md b/.gitea/issue_template/new-skill.md new file mode 100644 index 0000000..71e1a4f --- /dev/null +++ b/.gitea/issue_template/new-skill.md @@ -0,0 +1,36 @@ +--- +name: 新技能提案 +about: 提新技能或技能改动前先过 SCOPE.md 门槛(默认拒绝,能组合就不新增) +title: "[技能] " +labels: skill +--- + +## 1. 真实失败(必填) + +在哪个真实任务/会话里出了问题?跑了什么、期望什么、实际什么。 + +> 假设性的「这样会更好」不算。说不清就说明还不到提的时候。 + +## 2. 为什么组合不出来(必填) + +用现有哪个技能组合试过?为什么做不到? + +> 能组合出来的,改现有技能,不新增。 + +## 3. 是否重复提案 + +- [ ] 已读 `.out-of-scope/` 三份拒绝档案,不是重复提案 + +## 4. 分桶与调用模式 + +- 初始分桶:`beta`(新技能一律先 beta) +- 调用模式:`model-invoked` / `user-invoked` + - 判定:这件事是不是「人下命令才开始」?是 → `user-invoked`(同时写 `disable-model-invocation: true`) + +## 5. 最低交付 + +- [ ] `evals/evals.json` 3 条:正常路径 / 边界 / 失败恢复 +- [ ] `docs/skills/.md` 四段式:What it does / When to reach for it / Common questions / It's working if +- [ ] `docs/skill-routing.md` 已回改 +- [ ] `catalog.json` 已 regenerate +- [ ] `CHANGELOG.md` + `plugin.json` 版本号 diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..423c7a6 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,24 @@ +## 这个 PR 做了什么 + + + +## SCOPE 门槛 + +- [ ] 真实失败:说清了在哪个真实任务里出的问题 +- [ ] 组合不出来:说明用现有技能组合为什么做不到 +- [ ] 已查 `.out-of-scope/`,不是重复提案 + +## 改动检查 + +- [ ] 分桶正确(新技能一律 `beta`;`promoted` 需要真实使用记录 + evals 通过) +- [ ] 调用模式正确(`user-invoked` 必须写 `disable-model-invocation: true`) +- [ ] `evals/evals.json` 3 条(正常 / 边界 / 失败恢复) +- [ ] `docs/skills/.md` 四段式 +- [ ] `docs/skill-routing.md` 已回改 +- [ ] `catalog.json` 已 regenerate(`python3 scripts/generate-catalog.py`) +- [ ] `CHANGELOG.md` + `plugin.json` 版本号已更新 +- [ ] 四道门禁通过:`validate-skills.py --strict`、`scan-skill.sh`、`generate-catalog.py --check`、`tests/install-skills.test.sh` + +## 人工拍板项(有则写明) + + diff --git a/.gitignore b/.gitignore index 91acbae..dc06b43 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,9 @@ __pycache__/ # Node node_modules/ + +# 定时任务产出(本机运行产物,不回写仓库;evals 结果报告除外) +reports/skill-health/ +reports/monthly/ +reports/_scheduler/ +reports/evals/queue-*.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f02faa2..c6e735c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,5 +58,13 @@ - 新增 beta 技能 `diagnosing-bugs`(难查 bug 诊断纪律:先建红循环再推理),补齐 evals 与四段式文档;路由表同步。 - 新增 `scripts/monthly-review.sh`:一条命令跑月度审阅(覆盖率 / 扫描 / 第三方 / catalog / description 异常)。 + +### 治理落地:定时任务与提案入口(2026-11-01) + +- 新增 `scripts/scheduled-task.sh`:`daily` / `weekly` / `monthly` 三个确定性任务,只跑门禁、扫漂移、生成队列;需要模型判断的部分(真跑 evals、审阅 routing)生成 queue 文件交 agent。 +- 新增 `scripts/install-schedulers.sh`:把节拍装成 macOS 用户级 launchd(`com.pipexerp.ai-proj-skill-{daily,weekly,monthly}`),不再依赖会话提醒;支持 `--status` / `--run` / `--uninstall`。 +- 新增 `.gitea/issue_template/new-skill.md`、`.gitea/issue_template/config.yaml`、`.gitea/pull_request_template.md`:提案与 PR 走同一套 SCOPE 门槛问题。 +- `README.md` 一键部署从 2 步改为 4 步(补 git hooks 与定时任务);`MAINTENANCE.md` 增加「定时任务」一节,写明脚本只做确定性动作的边界。 +- `.gitignore` 排除定时任务产出(`reports/skill-health/`、`reports/monthly/`、`reports/_scheduler/`、`reports/evals/queue-*.md`)。 - 新增 `scripts/evals-status.py` 与 `reports/evals/TEMPLATE.md`:evals 覆盖率与回归报告模板。 - `skill-creator` 实战记录 #1:用标准流程新建 `diagnosing-bugs`(beta,待观察后晋升)。 diff --git a/MAINTENANCE.md b/MAINTENANCE.md index ec4a6bd..e2b5108 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -45,6 +45,27 @@ - 复核覆盖率:`--coverage` 的两项缺口是否按路线图收敛。 - 更新本文件与 `CHANGELOG.md`。 +## 定时任务(launchd,2026-11-01 起) + +节拍不再挂在「会话提醒」上(会话一断就失效),改成宿主级 launchd: + +```bash +./scripts/install-schedulers.sh # 安装(幂等;换机器重跑一次即可复现) +./scripts/install-schedulers.sh --status # 看状态与最近产出 +./scripts/install-schedulers.sh --run daily # 立刻跑一次(调试) +./scripts/install-schedulers.sh --uninstall +``` + +| 任务 | 时间 | 脚本做什么 | 产出 | +| --- | --- | --- | --- | +| `daily` | 每天 07:20 | 漂移 audit + 入库安全扫描 + 覆盖率 | `reports/skill-health/daily-YYYY-MM-DD.md`(`latest.md` 供 7:30 日报直接读) | +| `weekly` | 每周一 10:00 | 算上周改动技能 + 3 个高风险项,生成 evals 抽样队列 | `reports/evals/queue-YYYY-MM-DD.md` | +| `monthly` | 每月 1 日 10:00 | 一条命令跑完月度审阅;1/4/7/10 月附季度治理清单 | `reports/monthly/review-YYYY-MM.md`、`quarterly-YYYY-Qn.md` | + +**边界**:脚本只做确定性动作。真跑 evals、审阅 routing 这类需要模型判断的部分,脚本只生成队列文件,由项目经理 Bot 在 7:30 日报里接手执行——分工不写在脚本里就会漏。 + +日志:`reports/_scheduler/.log`。 + ## 责任分工 - 项目经理 Bot:执行节奏、出周/月报、盯漂移与门禁。 diff --git a/README.md b/README.md index 2935c34..f38b279 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ PRD 文档存储在思源笔记中,可以导出发送到飞书群,通过飞 - **Codex**([Skills 文档](https://developers.openai.com/codex/skills))或 **Claude Code** 已安装 - **ai-proj 账号 + MCP API Key**:联系管理员获取(Key 格式: `aiproj_pk_xxx`) -### 一键部署(2 步搞定) +### 一键部署(4 步搞定) ```bash # 1. 克隆仓库 @@ -78,8 +78,16 @@ cd ai-proj-helper # 2. 默认安装到 Codex 的用户级标准目录 ~/.agents/skills ./install-skills.sh + +# 3. 装本地门禁(Gitea Actions 无 runner,commit/push 靠 git hook 兜底) +./scripts/install-git-hooks.sh + +# 4. 装技能治理定时任务(macOS launchd:每日漂移检查 / 每周 evals 队列 / 每月审阅) +./scripts/install-schedulers.sh ``` +第 3、4 步只对「本仓库的维护者」需要——只装技能给别人用的,跑完第 2 步就可以停。 + 安装器会复制完整技能目录,包括 `SKILL.md`、references、scripts 和 assets。Codex 会自动发现 `~/.agents/skills` 中的技能;若没有出现,重启 Codex。 Claude Code 用户显式选择 Claude 目标;需要同时配置 MCP 和 marketplace 时运行 `init.sh`: diff --git a/scripts/install-schedulers.sh b/scripts/install-schedulers.sh new file mode 100755 index 0000000..1556ecc --- /dev/null +++ b/scripts/install-schedulers.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# 安装 / 卸载 / 查看技能治理的定时任务(macOS 用户级 launchd) +# +# 为什么用 launchd 而不是会话提醒:会话提醒随会话消失,节拍就断了; +# launchd 是宿主级,跟仓库绑定,换机器重跑一次即可复现。 +# +# 用法: +# ./scripts/install-schedulers.sh 安装(幂等,会先卸载再装) +# ./scripts/install-schedulers.sh --status 看三个任务的加载状态 +# ./scripts/install-schedulers.sh --run daily 立刻跑一次(调试用) +# ./scripts/install-schedulers.sh --uninstall 卸载 +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TASK="$ROOT/scripts/scheduled-task.sh" +AGENTS="$HOME/Library/LaunchAgents" +LOGDIR="$ROOT/reports/_scheduler" +PREFIX="com.pipexerp.ai-proj-skill" +UID_N="$(id -u)" +JOBS="daily weekly monthly" + +SCHED_PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" +if command -v node >/dev/null 2>&1; then + SCHED_PATH="$(dirname "$(command -v node)"):$SCHED_PATH" +fi + +log() { printf '%s\n' "$*"; } + +calendar_xml() { + case "$1" in + daily) printf ' Hour7\n Minute20' ;; + weekly) printf ' Weekday1\n Hour10\n Minute0' ;; + monthly) printf ' Day1\n Hour10\n Minute0' ;; + esac +} + +write_plist() { + local job="$1" label="$PREFIX-$job" file="$AGENTS/$PREFIX-$job.plist" cal + cal="$(calendar_xml "$job")" + cat > "$file" < + + + + Label$label + ProgramArguments + + /bin/bash + $TASK + $job + + WorkingDirectory$ROOT + EnvironmentVariables + + HOME$HOME + PATH$SCHED_PATH + + StandardOutPath$LOGDIR/$job.log + StandardErrorPath$LOGDIR/$job.err.log + RunAtLoad + StartCalendarInterval + +$cal + + + +PLIST +} + +unload_one() { + local label="$PREFIX-$1" + launchctl bootout "gui/$UID_N/$label" >/dev/null 2>&1 || launchctl unload "$AGENTS/$label.plist" >/dev/null 2>&1 +} + +install() { + mkdir -p "$AGENTS" "$LOGDIR" + chmod +x "$TASK" 2>/dev/null + local job label + for job in $JOBS; do + label="$PREFIX-$job" + write_plist "$job" + unload_one "$job" + if launchctl bootstrap "gui/$UID_N" "$AGENTS/$label.plist" 2>/dev/null; then + log "已加载 $label" + elif launchctl load -w "$AGENTS/$label.plist" 2>/dev/null; then + log "已加载(load) $label" + else + log "加载失败 $label(检查 plist: $AGENTS/$label.plist)" + fi + done + log "" + log "触发时间:daily 每天 07:20 / weekly 每周一 10:00 / monthly 每月 1 日 10:00(1/4/7/10 月附季度清单)" + log "日志:${LOGDIR#$ROOT/}/.log" + log "产出:reports/skill-health/、reports/evals/queue-*.md、reports/monthly/" +} + +status() { + local job label + for job in $JOBS; do + label="$PREFIX-$job" + if launchctl print "gui/$UID_N/$label" >/dev/null 2>&1; then + printf '%-8s 已加载 %s\n' "$job" \ + "$(launchctl print "gui/$UID_N/$label" 2>/dev/null | grep -E 'state =|last exit code' | tr -d '\t' | tr '\n' ' ')" + else + printf '%-8s 未加载\n' "$job" + fi + done + log "" + log "日志目录:${LOGDIR#$ROOT/}" + log "最近产出:" + ls -1t "$ROOT/reports/skill-health"/daily-*.md "$ROOT/reports/evals"/queue-*.md "$ROOT/reports/monthly"/review-*.md 2>/dev/null | head -3 | sed "s#^$ROOT/# - #" +} + +run_now() { + local job="${1:-}" + case "$job" in daily|weekly|monthly) ;; *) log "用法: $0 --run daily|weekly|monthly"; exit 2 ;; esac + mkdir -p "$LOGDIR" + local label="$PREFIX-$job" + if launchctl print "gui/$UID_N/$label" >/dev/null 2>&1; then + launchctl kickstart -k "gui/$UID_N/$label" && log "已触发 $job(看 ${LOGDIR#$ROOT/}/$job.log)" + else + log "任务未加载,直接前台执行" + /bin/bash "$TASK" "$job" + fi +} + +uninstall() { + local job label + for job in $JOBS; do + label="$PREFIX-$job" + unload_one "$job" + rm -f "$AGENTS/$label.plist" + log "已卸载 $label" + done +} + +case "${1:-install}" in + install|--install) install ;; + --status|status) status ;; + --run) run_now "${2:-}" ;; + --uninstall|uninstall) uninstall ;; + *) log "用法: $0 [install|--status|--run daily|weekly|monthly|--uninstall]"; exit 2 ;; +esac diff --git a/scripts/scheduled-task.sh b/scripts/scheduled-task.sh new file mode 100755 index 0000000..582ff73 --- /dev/null +++ b/scripts/scheduled-task.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# 技能治理定时任务入口(由 launchd 调用,见 scripts/install-schedulers.sh) +# +# 设计原则:脚本只做「确定性」的事——跑门禁、扫漂移、生成清单。 +# 需要模型判断的部分(真跑 evals、人工审阅 routing)不在脚本里做,只生成 queue 文件, +# 由项目经理 Bot 在 7:30 项目日报里接手执行。这样节拍不依赖任何会话的记忆。 +# +# 用法: +# ./scripts/scheduled-task.sh daily # 每天 07:20,先于 7:30 项目日报 +# ./scripts/scheduled-task.sh weekly # 每周一 10:00 +# ./scripts/scheduled-task.sh monthly # 每月 1 日 10:00(1/4/7/10 月附带季度清单) +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" || exit 1 + +JOB="${1:-daily}" +DAY="$(date '+%F')" +HEALTH_DIR="$ROOT/reports/skill-health" +EVALS_DIR="$ROOT/reports/evals" +MONTHLY_DIR="$ROOT/reports/monthly" +mkdir -p "$HEALTH_DIR" "$EVALS_DIR" "$MONTHLY_DIR" + +log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"; } + +# ---------- 公共采集:漂移 / 入库扫描 / 覆盖率 / 第三方 ---------- +DRIFT_LINE=""; SCAN_LINE=""; COV_LINE=""; THIRD_LINE="" +DRIFT_N="0"; HIGH_N="0" +collect_health() { + local audit + audit="$(./install-skills.sh --audit 2>&1)" + DRIFT_LINE="$(printf '%s\n' "$audit" | grep -E '[0-9]+ 个漂移' | tail -1)" + SCAN_LINE="$(./scripts/scan-skill.sh 2>&1 | grep '扫描完成' | tail -1)" + COV_LINE="$(/usr/bin/python3 scripts/validate-skills.py --coverage 2>&1 | grep '覆盖率' | tail -1)" + THIRD_LINE="$(./scripts/scan-installed.sh 2>&1 | grep '扫描完成' | tail -1)" + DRIFT_N="$(printf '%s' "$DRIFT_LINE" | grep -oE '[0-9]+ 个漂移' | grep -oE '^[0-9]+' || true)" + HIGH_N="$(printf '%s' "$SCAN_LINE" | grep -oE '[0-9]+ 个高危' | grep -oE '^[0-9]+' || true)" + DRIFT_N="${DRIFT_N:-0}" + HIGH_N="${HIGH_N:-0}" + AUDIT_OUT="$audit" +} + +# ---------- 每日:漂移 + 入库扫描,写一份可被日报直接读的健康快照 ---------- +run_daily() { + local md="$HEALTH_DIR/daily-$DAY.md" todo=0 + collect_health + { + echo "# 技能健康 $DAY" + echo + echo "- 漂移:${DRIFT_LINE:-未采集}" + echo "- 入库扫描:${SCAN_LINE:-未采集}" + echo "- 覆盖率:${COV_LINE:-未采集}" + echo "- 第三方技能:${THIRD_LINE:-未采集}" + echo + echo "## 需要处理" + if [ "$DRIFT_N" != "0" ]; then + echo "- 漂移 ${DRIFT_N} 个(未安装 / 版本落后),按 MAINTENANCE「每日」处理" + todo=1 + fi + if [ "$HIGH_N" != "0" ]; then + echo "- 入库扫描高危 ${HIGH_N} 个,立即处理,不等日报" + todo=1 + fi + [ "$todo" = "0" ] && echo "无" + echo + echo "## 原始输出(audit 尾部)" + echo '```' + printf '%s\n' "$AUDIT_OUT" | tail -15 + echo '```' + } > "$md" + cp "$md" "$HEALTH_DIR/latest.md" + log "daily -> ${md#$ROOT/}" + log "漂移 ${DRIFT_N} / 高危 ${HIGH_N} / ${SCAN_LINE:-扫描未采集}" +} + +# ---------- 每周:生成 evals 抽样队列(执行仍需模型,见 MAINTENANCE「每周一」)---------- +run_weekly() { + local md="$EVALS_DIR/queue-$DAY.md" + local changed changed_total highrisk reports + changed_total="$(git log --since='7 days ago' --name-only --pretty=format: 2>/dev/null \ + | grep -oE '^skills-[a-z]+/[^/]+-plugin/' | sort -u | wc -l | tr -d ' ')" + changed="$(git log --since='7 days ago' --name-only --pretty=format: 2>/dev/null \ + | grep -oE '^skills-[a-z]+/[^/]+-plugin/' \ + | sed -E 's#^skills-[a-z]+/##; s#-plugin/$##' \ + | sort | uniq -c | sort -rn | head -5 | awk '{print $2}' | tr '\n' ' ')" + highrisk="dev-coding dev-review req-design" + reports="$(ls "$EVALS_DIR"/*.md 2>/dev/null | grep -vE 'TEMPLATE|queue-' | wc -l | tr -d ' ')" + { + echo "# 本周 evals 抽样队列 $DAY" + echo + echo "现有回归报告:${reports:-0} 份(目标:每周至少 +1 份)" + echo + echo "## 范围" + echo + echo "- 上周改动技能共 ${changed_total:-0} 个,按改动次数取前 5:${changed:-(无)}" + echo "- 高风险固定项:$highrisk" + echo + echo "## 做法(每个技能)" + echo + echo "1. 取 1 条关键用例,跑「有技能 / 无技能」两遍;" + echo "2. 按 rubric 打分,写增量;" + echo "3. 结果写入 \`reports/evals/$DAY-<技能名>.md\`(模板 \`reports/evals/TEMPLATE.md\`)。" + echo + echo "## 判读" + echo + echo "- 连续两次无增量 → 按 \`SCOPE.md\` 评估降级。" + echo "- 全量只在治理大改时做,不做每周全量。" + echo + echo "> 本文件由 launchd 每周一 10:00 自动生成;脚本不调模型,执行由 agent 接手。" + } > "$md" + log "weekly -> ${md#$ROOT/}(报告现有 ${reports:-0} 份)" +} + +# ---------- 每月:一条命令出齐审阅项;季度月附治理清单 ---------- +run_monthly() { + local mo md mon q + mo="$(date '+%Y-%m')" + mon="$(date '+%m')" + md="$MONTHLY_DIR/review-$mo.md" + { + echo "# 技能月度审阅 $mo" + echo + ./scripts/monthly-review.sh 2>&1 + } > "$md" + collect_health + { + echo + echo "## 补充:漂移与第三方" + echo + echo "- ${DRIFT_LINE:-漂移未采集}" + echo "- ${THIRD_LINE:-第三方未采集}" + } >> "$md" + log "monthly -> ${md#$ROOT/}" + case "$mon" in + 01|04|07|10) + q="$(date '+%Y')-Q$(( (10#$mon - 1) / 3 + 1 ))" + local mq="$MONTHLY_DIR/quarterly-$q.md" + { + echo "# 季度治理复核 $q" + echo + echo "按 MAINTENANCE「每季度首月 1 日 10:30」逐项打勾:" + echo + echo "- [ ] 过一遍 \`docs/skill-routing.md\`:删掉不再用的引用、补上新技能" + echo "- [ ] 清理 \`deprecated\`:满 3 个月且无回滚需求 → 删除插件目录" + echo "- [ ] 复核分桶与调用模式:还有没有命令型技能占着 \`model-invoked\`" + echo "- [ ] 复核覆盖率:\`--coverage\` 两项缺口是否按路线图收敛" + echo "- [ ] 更新 \`MAINTENANCE.md\` 与 \`CHANGELOG.md\`" + } > "$mq" + log "quarterly -> ${mq#$ROOT/}" + ;; + esac +} + +case "$JOB" in + daily) run_daily ;; + weekly) run_weekly ;; + monthly) run_monthly ;; + *) + echo "用法: $0 daily|weekly|monthly" >&2 + exit 2 + ;; +esac