diff --git a/docs/standards/playbook/.gitea/ci/sync_superpowers.sh b/docs/standards/playbook/.gitea/ci/sync_superpowers.sh index d9920ea..10ed891 100644 --- a/docs/standards/playbook/.gitea/ci/sync_superpowers.sh +++ b/docs/standards/playbook/.gitea/ci/sync_superpowers.sh @@ -6,8 +6,8 @@ SUPERPOWERS_BRANCH="${SUPERPOWERS_BRANCH:-thirdparty/skill}" SUPERPOWERS_DIR="${SUPERPOWERS_DIR:-superpowers}" SUPERPOWERS_LIST="${SUPERPOWERS_LIST:-codex/skills/.sources/superpowers.list}" TARGET_BRANCH="${TARGET_BRANCH:-main}" -COMMIT_AUTHOR_NAME="${COMMIT_AUTHOR_NAME:-playbook-bot}" -COMMIT_AUTHOR_EMAIL="${COMMIT_AUTHOR_EMAIL:-playbook-bot@local}" +COMMIT_AUTHOR_NAME="${COMMIT_AUTHOR_NAME:-ci-bot}" +COMMIT_AUTHOR_EMAIL="${COMMIT_AUTHOR_EMAIL:-ci-bot@local}" cd "$REPO_DIR" diff --git a/docs/standards/playbook/.gitea/ci/update_thirdparty_superpowers.sh b/docs/standards/playbook/.gitea/ci/update_thirdparty_superpowers.sh new file mode 100644 index 0000000..c558ba4 --- /dev/null +++ b/docs/standards/playbook/.gitea/ci/update_thirdparty_superpowers.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_DIR="${REPO_DIR:-$(pwd)}" +TARGET_BRANCH="${TARGET_BRANCH:-thirdparty/skill}" +SNAPSHOT_DIR="${SNAPSHOT_DIR:-superpowers}" +SOURCE_FILE="${SOURCE_FILE:-${SNAPSHOT_DIR}/SOURCE.md}" +UPSTREAM_REPO="${UPSTREAM_REPO:-https://github.com/obra/superpowers.git}" +UPSTREAM_REF="${UPSTREAM_REF:-main}" +COMMIT_AUTHOR_NAME="${COMMIT_AUTHOR_NAME:-ci-bot}" +COMMIT_AUTHOR_EMAIL="${COMMIT_AUTHOR_EMAIL:-ci-bot@local}" + +retry_cmd() { + local retries="$1" + shift + local delay="$1" + shift + + local attempt=1 + while true; do + if "$@"; then + return 0 + fi + if [ "$attempt" -ge "$retries" ]; then + return 1 + fi + echo "Retry ($attempt/$retries): $*" >&2 + sleep "$delay" + attempt=$((attempt + 1)) + done +} + +github_owner_repo() { + case "$1" in + https://github.com/*) + echo "$1" | sed -E 's#^https://github.com/([^/]+/[^/.]+)(\.git)?$#\1#' + ;; + http://github.com/*) + echo "$1" | sed -E 's#^http://github.com/([^/]+/[^/.]+)(\.git)?$#\1#' + ;; + git@github.com:*) + echo "$1" | sed -E 's#^git@github.com:([^/]+/[^/.]+)(\.git)?$#\1#' + ;; + *) + return 1 + ;; + esac +} + +resolve_latest_sha() { + local repo="$1" + local ref="$2" + local tmp_json="$3" + local gh_repo="$4" + local sha="" + + # Prefer GitHub API when possible to avoid git+gnutls handshake failures. + if [ -n "$gh_repo" ]; then + local api_url="https://api.github.com/repos/${gh_repo}/commits/${ref}" + if retry_cmd 3 2 curl -fsSL --retry 3 --retry-delay 2 "$api_url" -o "$tmp_json"; then + sha="$(sed -n 's/^[[:space:]]*"sha":[[:space:]]*"\([0-9a-f]\{40\}\)".*/\1/p' "$tmp_json" | head -n 1)" + if [ -n "$sha" ]; then + echo "$sha" + return 0 + fi + fi + fi + + # Fallback to git transport. + sha="$(retry_cmd 3 2 git -c http.version=HTTP/1.1 ls-remote "$repo" "refs/heads/$ref" | awk 'NR==1 {print $1}')" + if [ -n "$sha" ]; then + echo "$sha" + return 0 + fi + return 1 +} + +cd "$REPO_DIR" + +git config user.name "$COMMIT_AUTHOR_NAME" +git config user.email "$COMMIT_AUTHOR_EMAIL" + +git fetch origin "$TARGET_BRANCH" +git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH" + +tmp_dir="$(mktemp -d)" +cleanup() { + rm -rf "$tmp_dir" +} +trap cleanup EXIT + +gh_repo="" +if gh_repo="$(github_owner_repo "$UPSTREAM_REPO" 2>/dev/null)"; then + : +fi + +latest_sha="$(resolve_latest_sha "$UPSTREAM_REPO" "$UPSTREAM_REF" "$tmp_dir/latest.json" "$gh_repo" || true)" +if [ -z "$latest_sha" ]; then + echo "ERROR: failed to resolve upstream ref: $UPSTREAM_REPO $UPSTREAM_REF" >&2 + exit 1 +fi + +current_sha="" +if [ -f "$SOURCE_FILE" ]; then + current_sha="$(sed -n 's/^- Ref:[[:space:]]*//p' "$SOURCE_FILE" | head -n 1)" +fi + +if [ "$latest_sha" = "$current_sha" ]; then + echo "Third-party snapshot is up to date: $latest_sha" + exit 0 +fi + +rm -rf "$SNAPSHOT_DIR" +mkdir -p "$SNAPSHOT_DIR" + +snapshot_loaded=0 + +if [ -n "$gh_repo" ]; then + tar_url="https://codeload.github.com/${gh_repo}/tar.gz/${latest_sha}" + if retry_cmd 3 2 curl -fsSL --retry 3 --retry-delay 2 "$tar_url" -o "$tmp_dir/upstream.tar.gz"; then + tar -xzf "$tmp_dir/upstream.tar.gz" -C "$SNAPSHOT_DIR" --strip-components=1 + snapshot_loaded=1 + fi +fi + +if [ "$snapshot_loaded" -eq 0 ]; then + upstream_dir="$tmp_dir/upstream" + git init "$upstream_dir" >/dev/null + git -C "$upstream_dir" remote add origin "$UPSTREAM_REPO" + retry_cmd 3 2 git -C "$upstream_dir" fetch --depth 1 origin "$latest_sha" + git -C "$upstream_dir" checkout --detach FETCH_HEAD + git -C "$upstream_dir" archive --format=tar HEAD | tar -xf - -C "$SNAPSHOT_DIR" +fi + +snapshot_date="$(date -u +%Y-%m-%d)" +cat > "$SOURCE_FILE" </dev/null; then - git checkout -f "$TARGET_SHA" - else - if [ -n "$TARGET_REF" ]; then - git fetch origin "$TARGET_REF" - git checkout -f FETCH_HEAD - else - git checkout -f "${{ github.ref_name }}" - fi - fi + # Always run sync from the latest target branch state. + # This prevents stale/manual dispatch refs from using outdated scripts. + git fetch origin "${{ env.TARGET_BRANCH }}" + git checkout -B "${{ env.TARGET_BRANCH }}" "origin/${{ env.TARGET_BRANCH }}" git config --global --add safe.directory "$REPO_DIR" echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV diff --git a/docs/standards/playbook/.gitea/workflows/update-thirdparty-superpowers.yml b/docs/standards/playbook/.gitea/workflows/update-thirdparty-superpowers.yml new file mode 100644 index 0000000..0c8dcf4 --- /dev/null +++ b/docs/standards/playbook/.gitea/workflows/update-thirdparty-superpowers.yml @@ -0,0 +1,68 @@ +name: Update Third-party Superpowers + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: thirdparty-superpowers-update-${{ github.repository }} + cancel-in-progress: true + +env: + WORKSPACE_DIR: "/home/workspace" + TARGET_BRANCH: "thirdparty/skill" + UPSTREAM_REPO: "https://github.com/obra/superpowers.git" + UPSTREAM_REF: "main" + +jobs: + update: + name: Update thirdparty/skill snapshot + runs-on: ubuntu-22.04 + + steps: + - name: Prepare repo + run: | + echo "========================================" + echo "Prepare repo to WORKSPACE_DIR" + echo "========================================" + + REPO_NAME="${{ github.event.repository.name }}" + REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME" + TOKEN="${{ secrets.WORKFLOW }}" + if [ -n "$TOKEN" ]; then + REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git" + else + REPO_URL="${GITHUB_SERVER_URL}/${{ github.repository }}.git" + fi + + if [ -d "$REPO_DIR" ]; then + if [ -d "$REPO_DIR/.git" ]; then + cd "$REPO_DIR" + git clean -fdx + git reset --hard + git fetch --all --tags --force --prune --prune-tags + else + rm -rf "$REPO_DIR" + fi + fi + + if [ ! -d "$REPO_DIR/.git" ]; then + mkdir -p "${{ env.WORKSPACE_DIR }}" + git clone "$REPO_URL" "$REPO_DIR" + cd "$REPO_DIR" + fi + + git fetch origin main + git checkout -B main origin/main + + git config --global --add safe.directory "$REPO_DIR" + echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV + + - name: Update thirdparty/skill snapshot + shell: bash + run: | + set -euo pipefail + cd "$REPO_DIR" + bash .gitea/ci/update_thirdparty_superpowers.sh diff --git a/docs/standards/playbook/README.md b/docs/standards/playbook/README.md index 77a8d89..92d51e4 100644 --- a/docs/standards/playbook/README.md +++ b/docs/standards/playbook/README.md @@ -1,6 +1,13 @@ # playbook -Playbook:TSL(`.tsl`/`.tsf`)+ C++ + Python + Markdown(代码格式化)工程规范与代理规则合集。 +Playbook:工程规范与代理规则合集,当前覆盖: + +- TSL(`.tsl`/`.tsf`) +- C++ +- Python +- TypeScript(`.ts`/`.tsx`) +- JavaScript(`.js`/`.mjs`/`.cjs`) +- Markdown(代码格式化) ## 原则 @@ -10,14 +17,14 @@ Playbook:TSL(`.tsl`/`.tsf`)+ C++ + Python + Markdown(代码格式化) ## 适用范围 -- 本指南适用于所有 TSL/C++/Python/Markdown 相关仓库与脚本 +- 本指南适用于所有 TSL/C++/Python/TypeScript/JavaScript/Markdown 相关仓库与脚本 - 当现有代码与本指南冲突时,**以保持局部一致性为优先**,逐步迁移 ## docs/(开发规范) `docs/` 目录是给开发者阅读的工程规范,约束代码写法、命名与提交信息。 -- `docs/index.md`:文档导航(跨语言 common / TSL / C++ / Python / Markdown)。 +- `docs/index.md`:文档导航(跨语言 common / TSL / C++ / Python / TypeScript / Markdown)。 - `docs/common/commit_message.md`:提交信息与版本号规范(type/scope/subject/body/footer、可选 Emoji 图例、SemVer)。 - `docs/tsl/code_style.md`:TSL 代码结构、格式、`begin/end` 代码块、注释与通用最佳实践。 @@ -35,6 +42,10 @@ Playbook:TSL(`.tsl`/`.tsf`)+ C++ + Python + Markdown(代码格式化) - `docs/python/configuration.md`:Python 配置清单(落地时从 `templates/python/` 复制到项目根目录)。 - `docs/markdown/index.md`:Markdown 代码块与行内代码格式(仅代码格式化)。 +- `docs/typescript/code_style.md`:TypeScript 代码风格(Google 基线)。 +- `docs/typescript/naming.md`:TypeScript 命名规范。 +- `docs/typescript/toolchain.md`:TypeScript 工具链(typescript/prettier/eslint/vitest)。 +- `docs/typescript/configuration.md`:TypeScript 配置清单(tsconfig/eslint/prettier)。 - `templates/cpp/`:C++ 落地模板(`.clang-format`、`conanfile.txt`、`CMakeUserPresets.json`、`CMakeLists.txt`)。 - `templates/python/`:Python 落地模板(`pyproject.toml` 工具配置、`.flake8`、`.pylintrc`、`.pre-commit-config.yaml`、`.editorconfig`、`.vscode/settings.json`)。 @@ -124,6 +135,7 @@ Layer 3: docs/ (权威静态文档) - `rulesets/tsl/index.md`:TSL 核心约定(44 行) - `rulesets/cpp/index.md`:C++ 核心约定(47 行) - `rulesets/python/index.md`:Python 核心约定(45 行) +- `rulesets/typescript/index.md`:TypeScript 核心约定(47 行) - `rulesets/markdown/index.md`:Markdown 核心约定(31 行,仅代码格式化) 更多说明:`rulesets/index.md` @@ -287,16 +299,16 @@ python scripts/playbook.py -config playbook.toml - 行尾与文本规范:`.gitattributes` - 代理最低要求:`.agents/*`(工作原则、质量底线、安全边界) 2. **语言级(Language-specific)规范**:只对某个语言成立的风格与工具。 - - 例如 TSL 的命名/文件顶层声明限制、C++ 的 `.clang-format/.clang-tidy`、Python 的 `ruff` 等。 + - 例如 TSL 的命名/文件顶层声明限制、C++ 的 `.clang-format/.clang-tidy`、Python 的 `ruff`、TypeScript 的 ESLint/类型约束等。 **建议**:仓库级规则尽量少且稳定;语言级规则各自独立,避免互相"污染"。 -本仓库提供多套代理规则集(同步后位于目标项目的 `.agents/tsl/` / `.agents/cpp/` / `.agents/python/` / `.agents/markdown/`): +本仓库提供多套代理规则集(同步后位于目标项目的 `.agents/tsl/` / `.agents/cpp/` / `.agents/python/` / `.agents/typescript/` / `.agents/markdown/`): - 三者都包含核心约定与安全红线 -- 并在 `index.md` 中叠加语言级"硬约束"(TSL/TSF 语法限制、C++23/Modules、Python 风格、Markdown 代码格式化等) +- 并在 `index.md` 中叠加语言级"硬约束"(TSL/TSF 语法限制、C++23/Modules、Python 风格、TypeScript 类型约束、Markdown 代码格式化等) -**多语言项目推荐结构**(示例:TSL + C++ + Python + Markdown): +**多语言项目推荐结构**(示例:TSL + C++ + Python + TypeScript + Markdown): ```txt . @@ -305,6 +317,7 @@ python scripts/playbook.py -config playbook.toml │ ├── tsl/ # 由本 Playbook 同步(适用于 .tsl/.tsf) │ ├── cpp/ # 由本 Playbook 同步(适用于 C++23/Modules) │ ├── python/ # Python 规则集(同上) +│ ├── typescript/ # TypeScript/JavaScript 规则集(同上) │ └── markdown/ # Markdown 规则集(仅代码格式化) ├── .gitattributes # 行尾/文本规范 ├── docs/ diff --git a/docs/standards/playbook/SKILLS.md b/docs/standards/playbook/SKILLS.md index 2bea1e8..8c0b5ee 100644 --- a/docs/standards/playbook/SKILLS.md +++ b/docs/standards/playbook/SKILLS.md @@ -4,8 +4,8 @@ 并给出与本 Playbook(`docs/` + `rulesets/`)配套的技能编写建议与内置技能清单。 > 提示:Codex skills 是“按用户安装”的(默认在 -> `~/.codex/skills`)。本仓库将 skills 以可分发的形式放在 -> `codex/skills/`,并提供脚本一键安装到你的 `CODEX_HOME`。 +> `~/.agents/skills`)。本仓库将 skills 以可分发的形式放在 +> `codex/skills/`,并提供脚本一键安装到你的 `~/.agents`。 --- @@ -38,14 +38,14 @@ codex/skills/ 最终安装到本机后,对应路径为: ```txt -$CODEX_HOME/skills//SKILL.md +~/.agents/skills//SKILL.md ``` --- ## 3. 安装到本机(推荐) -使用统一入口 `playbook.py` 安装 skills(会把 `codex/skills/*` 复制到 `$CODEX_HOME/skills/`): +使用统一入口 `playbook.py` 安装 skills(会把 `codex/skills/*` 复制到 `~/.agents/skills/`): ```toml # playbook.toml @@ -54,7 +54,7 @@ project_root = "." [install_skills] mode = "all" # list|all -codex_home = "~/.codex" +agents_home = "~/.agents" ``` ```bash @@ -74,10 +74,10 @@ skills = ["style-cleanup", "commit-message"] ```toml [install_skills] mode = "all" -codex_home = "./.codex" +agents_home = "./.agents" ``` -> 注意:Codex 只会从 `CODEX_HOME` 加载 skills;使用本地安装时,启动 Codex 需设置同样的 `CODEX_HOME`。 +> 注意:Codex 默认从 `~/.agents/skills` 加载 skills;使用本地安装时,需要确保 Codex 能发现该路径。 如果你的项目通过 `git subtree` vendoring 本 Playbook(推荐前缀 `docs/standards/playbook`),则在目标项目里执行: @@ -161,22 +161,29 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml 来源:`codex/skills/.sources/superpowers.list`(第三方来源清单)。 本节仅列出 superpowers 体系 skills,与本 Playbook 原生 skills 分离。 - +### Third-party Skills (superpowers) -- brainstorming -- dispatching-parallel-agents -- executing-plans -- finishing-a-development-branch -- receiving-code-review -- requesting-code-review -- subagent-driven-development -- systematic-debugging -- test-driven-development -- using-git-worktrees -- using-superpowers -- verification-before-completion -- writing-plans -- writing-skills +### Third-party Skills (superpowers) + +### Third-party Skills (superpowers) + +### Third-party Skills (superpowers) + + +- \ +- \ +- \ +- \ +- \ +- \ +- \ +- \ +- \ +- \ +- \ +- \ +- \ +- \ --- diff --git a/docs/standards/playbook/docs/index.md b/docs/standards/playbook/docs/index.md index 57c1cb7..d3e56c4 100644 --- a/docs/standards/playbook/docs/index.md +++ b/docs/standards/playbook/docs/index.md @@ -33,6 +33,13 @@ - 工具链:`python/tooling.md` - 配置清单:`python/configuration.md` +## TypeScript(typescript) + +- 代码风格:`typescript/code_style.md` +- 命名规范:`typescript/naming.md` +- 工具链:`typescript/toolchain.md` +- 配置清单:`typescript/configuration.md` + ## Markdown(markdown) - 代码块与行内代码格式:`markdown/index.md` diff --git a/docs/standards/playbook/docs/typescript/code_style.md b/docs/standards/playbook/docs/typescript/code_style.md new file mode 100644 index 0000000..ab08d64 --- /dev/null +++ b/docs/standards/playbook/docs/typescript/code_style.md @@ -0,0 +1,37 @@ +# TypeScript/JavaScript 代码风格 + +本 Playbook 的 TypeScript/JavaScript 代码风格以 Google TypeScript Style Guide 为基线: + +- Google TypeScript Style Guide: https://google.github.io/styleguide/tsguide.html +- TypeScript 官方手册: https://www.typescriptlang.org/docs/handbook/ + +## 项目约定 + +- 适用范围:业务代码默认使用 `.ts/.tsx`;`.js/.mjs/.cjs` 仅用于脚本、工具链或配置 +- 行宽:100(与 Prettier 配置保持一致) +- 缩进:2 空格 +- 引号:单引号(Prettier `singleQuote: true`) +- 分号:不加(Prettier `semi: false`);如仓库已有配置则以仓库为准 +- 尾随逗号:`all`(ES5+) + +## 类型约定 + +- TypeScript 禁止 `any`;需要宽泛类型时用 `unknown` 并做类型收窄 +- 优先使用 `interface` 描述对象结构;`type` 用于联合/交叉/工具类型 +- 函数返回类型:公共 API 必须显式标注;内部实现可依赖推断 +- 泛型参数名:单字母(`T`、`K`、`V`)或描述性名称(`TItem`) +- JavaScript 文件建议启用 `// @ts-check` 与 JSDoc 进行静态检查 + +## 模块约定 + +- 使用 ES Module(`import`/`export`);禁止 `require()`(除 `.cjs` 文件) +- 每文件一个主要导出;避免桶文件(`index.ts`/`index.js` re-export 过多) +- 路径别名:以 `tsconfig.json` 的 `paths` 为准 + +## 异步约定 + +- 优先 `async/await`;避免裸 `.then()/.catch()` 链 +- `async` 函数必须处理错误(`try/catch` 或调用方捕获) +- 不得忽略 Promise(使用 `void` 操作符显式标记有意忽略) + +当既有代码与本约定冲突时,优先保持局部一致性,逐步迁移。 diff --git a/docs/standards/playbook/docs/typescript/configuration.md b/docs/standards/playbook/docs/typescript/configuration.md new file mode 100644 index 0000000..c033fe7 --- /dev/null +++ b/docs/standards/playbook/docs/typescript/configuration.md @@ -0,0 +1,88 @@ +# TypeScript 配置清单 + +本文件汇总 TypeScript 项目常用配置文件说明。 + +## 1) `tsconfig.json` + +关键编译选项: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} +``` + +- `strict: true`:启用全部严格检查(必须) +- `noUncheckedIndexedAccess`:数组/对象索引访问返回 `T | undefined`(推荐) +- `skipLibCheck`:跳过 `.d.ts` 检查,加快编译 + +## 2) `.prettierrc` + +```json +{ + "semi": false, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "all", + "tabWidth": 2 +} +``` + +## 3) `eslint.config.js`(Flat Config) + +```js +import tseslint from 'typescript-eslint' + +export default tseslint.config( + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { projectService: true }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-floating-promises': 'error', + }, + }, +) +``` + +## 4) `.editorconfig` + +```ini +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{ts,tsx,js,jsx,json}] +indent_style = space +indent_size = 2 +max_line_length = 100 +``` + +## 5) `.vscode/settings.json` + +```json +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.rulers": [100], + "typescript.tsdk": "node_modules/typescript/lib" +} +``` diff --git a/docs/standards/playbook/docs/typescript/naming.md b/docs/standards/playbook/docs/typescript/naming.md new file mode 100644 index 0000000..2e92473 --- /dev/null +++ b/docs/standards/playbook/docs/typescript/naming.md @@ -0,0 +1,42 @@ +# TypeScript 命名规范 + +## 文件与目录 + +- 文件名:`kebab-case.ts` / `kebab-case.tsx` / `kebab-case.js` / `kebab-case.jsx` +- 脚本/配置文件:`kebab-case.mjs` / `kebab-case.cjs` +- 测试文件:`kebab-case.test.ts` / `kebab-case.spec.ts` / `kebab-case.test.js` / `kebab-case.spec.js` +- 类型声明文件:`kebab-case.d.ts` +- 目录名:`kebab-case/` + +## 标识符 + +| 类别 | 风格 | 示例 | +| ----------------- | -------------------------- | ------------------------------ | +| 类(Class) | `PascalCase` | `UserService` | +| 接口(Interface) | `PascalCase` | `UserProfile`(不加 `I` 前缀) | +| 类型别名(Type) | `PascalCase` | `ApiResponse` | +| 枚举(Enum) | `PascalCase` | `HttpStatus` | +| 枚举成员 | `UPPER_WITH_UNDER` | `NOT_FOUND` | +| 函数/方法 | `camelCase` | `getUserById` | +| 变量/参数 | `camelCase` | `userId` | +| 常量(模块级) | `UPPER_WITH_UNDER` | `MAX_RETRY_COUNT` | +| 私有成员 | `_camelCase` | `_cache` | +| 泛型参数 | 单字母或 `T` 前缀 | `T`、`TItem`、`K`、`V` | +| React 组件 | `PascalCase` | `UserCard` | +| React Hook | `camelCase`,以 `use` 开头 | `useUserData` | + +## 布尔值命名 + +布尔变量/属性以 `is`、`has`、`can`、`should` 开头: + +```ts +const isLoading = true +const hasPermission = false +``` + +## 避免 + +- 单字母变量(循环索引 `i`/`j` 除外) +- 缩写(`usr`、`btn`);优先完整单词 +- 匈牙利命名法(`strName`、`nCount`) +- 接口名加 `I` 前缀(`IUserService`) diff --git a/docs/standards/playbook/docs/typescript/toolchain.md b/docs/standards/playbook/docs/typescript/toolchain.md new file mode 100644 index 0000000..f2a38e9 --- /dev/null +++ b/docs/standards/playbook/docs/typescript/toolchain.md @@ -0,0 +1,54 @@ +# TypeScript/JavaScript 工具链 + +本 Playbook 推荐以下工具保证代码一致性与质量: + +- `typescript`:编译器(`tsc`) +- `prettier`:格式化 +- `eslint`:风格检查与静态分析(配合 `@typescript-eslint`) +- `vitest` / `jest`:测试(按项目选择) +- `tsx` / `ts-node`:直接运行 `.ts` 文件(开发/脚本场景) +- `node`:运行 `.js/.mjs/.cjs` 脚本 + +## 常用命令(示例) + +安装工具(按项目实际包管理器调整): + +```bash +pnpm add -D typescript prettier eslint typescript-eslint +``` + +类型检查: + +```bash +tsc --noEmit +``` + +JavaScript 检查(可选,启用 `allowJs` + `checkJs`): + +```bash +tsc --noEmit --allowJs --checkJs +``` + +格式化: + +```bash +prettier --write . +``` + +Lint 检查: + +```bash +eslint . +``` + +运行测试: + +```bash +vitest run +# 或 +jest --ci +``` + +## 包管理器 + +优先使用仓库已有的包管理器(`pnpm` / `npm` / `yarn`);未经沟通不切换。 diff --git a/docs/standards/playbook/playbook.toml.example b/docs/standards/playbook/playbook.toml.example index fd09ce1..6771f28 100644 --- a/docs/standards/playbook/playbook.toml.example +++ b/docs/standards/playbook/playbook.toml.example @@ -14,6 +14,7 @@ [sync_rules] # 同步 AGENT_RULES.md(配置节存在即启用) # force = false # 可选:覆盖已有文件 +# no_backup = false # 可选:跳过备份 [sync_memory_bank] # 同步 memory-bank/(配置节存在即启用) @@ -30,13 +31,14 @@ # no_backup = false # 可选:跳过备份 [sync_standards] -# langs = ["tsl", "cpp"] # 必填:要同步的语言 +# langs = ["tsl", "cpp", "typescript"] # 必填:要同步的语言 # gitattr_mode = "append" # append(补全缺失)|overwrite(覆盖)|block(插入块)|skip(跳过) +# no_backup = false # 可选:跳过备份(.agents/.gitattributes) [install_skills] # mode = "list" # list|all # skills = ["brainstorming"] # mode=list 时必填 -# codex_home = "~/.codex" # 可选:默认 ~/.codex +# agents_home = "~/.agents" # 可选:默认 ~/.agents [format_md] # tool = "prettier" # 仅支持 prettier diff --git a/docs/standards/playbook/rulesets/index.md b/docs/standards/playbook/rulesets/index.md index 3a8de3f..4957191 100644 --- a/docs/standards/playbook/rulesets/index.md +++ b/docs/standards/playbook/rulesets/index.md @@ -18,6 +18,7 @@ - `rulesets/tsl/`:TSL 相关规则集(适用于 `.tsl`/`.tsf`) - `rulesets/cpp/`:C++ 相关规则集(C++23,含 Modules) - `rulesets/python/`:Python 相关规则集 +- `rulesets/typescript/`:TypeScript/JavaScript 相关规则集 - `rulesets/markdown/`:Markdown 相关规则集(仅代码格式化) 目标项目落地时,通过 `scripts/playbook.py` 的 `[sync_standards]` diff --git a/docs/standards/playbook/rulesets/typescript/index.md b/docs/standards/playbook/rulesets/typescript/index.md new file mode 100644 index 0000000..c912358 --- /dev/null +++ b/docs/standards/playbook/rulesets/typescript/index.md @@ -0,0 +1,48 @@ +# TypeScript 代理规则集 + +本规则集定义 AI/自动化代理在处理 TypeScript/JavaScript 代码时必须遵守的核心约束。 + +## 范围与优先级 + +- 作为仓库级基线规则集使用;更靠近代码目录的规则更具体并可覆盖基线。 +- 当代理规则与 docs 冲突:安全/合规优先,其次保持仓库一致性。 + +## 代理工作原则(铁律) + +1. 先理解目标与上下文,再动手改代码 +2. 修改要小而清晰;避免无关重构 +3. 发现安全问题(明文密钥/鉴权漏洞)立即标注或修复 +4. 不引入新依赖或工具,除非明确要求 + +## TypeScript/JavaScript 核心约定(不可违反) + +- 语言标准:业务代码优先 TypeScript(`.ts/.tsx`);JavaScript(`.js/.mjs/.cjs`)仅用于脚本、配置或兼容场景 +- 类型:禁止使用 `any`;优先 `unknown`;所有公共 API 必须有明确类型注解 +- 代码风格:以仓库既有 ESLint/Prettier 配置为准;未经沟通不切换 formatter/linter +- Import:使用 ES Module(`import`/`export`);避免 `require()`;按 外部 → 内部 → 相对路径 分组 +- 命名:文件 `kebab-case.ts/.js`;类/接口/类型 `PascalCase`;函数/变量 `camelCase`;常量 `UPPER_WITH_UNDER`;私有成员 `_camelCase` +- 异步:优先 `async/await`;避免裸 `.then()` 链;错误必须处理 +- JavaScript 质量底线:必须通过 ESLint;推荐启用 `@ts-check` + JSDoc 做静态检查 + +## 安全红线(不可触碰) + +- 不得在代码/日志/注释中写入明文密钥、密码、Token、API Key +- 不得使用 `eval()` / `new Function()` 处理不可信输入 +- 不得直接拼接用户输入到 SQL/命令/HTML(防注入/XSS) +- 修改鉴权/权限逻辑必须说明动机与风险 + +## 权威来源 + +- 代码风格:`docs/typescript/code_style.md` +- 命名规范:`docs/typescript/naming.md` +- 工具链:`docs/typescript/toolchain.md` +- 配置清单:`docs/typescript/configuration.md` + +## Skills(按需加载) + +- `$commit-message` + +## 与开发规范的关系 + +- 本仓库内:`docs/typescript/` 与 `docs/common/` +- 目标项目 subtree:`docs/standards/playbook/docs/typescript/` 与 `docs/standards/playbook/docs/common/` diff --git a/docs/standards/playbook/scripts/playbook.py b/docs/standards/playbook/scripts/playbook.py index 51d001c..bd0f064 100644 --- a/docs/standards/playbook/scripts/playbook.py +++ b/docs/standards/playbook/scripts/playbook.py @@ -269,6 +269,16 @@ def write_docs_index(dest_prefix: Path, langs: list[str]) -> None: "- 工具链:`python/tooling.md`", "- 配置清单:`python/configuration.md`", ] + elif lang == "typescript": + lines += [ + "", + "## TypeScript(typescript)", + "", + "- 代码风格:`typescript/code_style.md`", + "- 命名规范:`typescript/naming.md`", + "- 工具链:`typescript/toolchain.md`", + "- 配置清单:`typescript/configuration.md`", + ] elif lang == "markdown": lines += [ "", @@ -832,6 +842,7 @@ def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str | "- `.agents/tsl/`:TSL 相关规则集(由 playbook 同步;适用于 `.tsl`/`.tsf`)", "- `.agents/cpp/`:C++ 相关规则集(由 playbook 同步;适用于 C++23/Modules)", "- `.agents/python/`:Python 相关规则集(由 playbook 同步)", + "- `.agents/typescript/`:TypeScript/JavaScript 相关规则集(由 playbook 同步)", "- `.agents/markdown/`:Markdown 相关规则集(仅代码格式化)", "", "规则发生冲突时,建议以“更靠近代码的目录规则更具体”为准。", @@ -856,6 +867,7 @@ def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None: "`docs/tsl/": f"`{docs_prefix}/tsl/", "`docs/cpp/": f"`{docs_prefix}/cpp/", "`docs/python/": f"`{docs_prefix}/python/", + "`docs/typescript/": f"`{docs_prefix}/typescript/", "`docs/markdown/": f"`{docs_prefix}/markdown/", "`docs/common/": f"`{docs_prefix}/common/", } @@ -1056,16 +1068,19 @@ def normalize_globs(raw: object) -> list[str]: def install_skills_action(config: dict, context: dict) -> int: mode = str(config.get("mode", "list")).lower() - codex_home = Path(config.get("codex_home", "~/.codex")).expanduser() - if not codex_home.is_absolute(): - codex_home = (context["project_root"] / codex_home).resolve() + if "codex_home" in config: + print("ERROR: codex_home is no longer supported; use agents_home", file=sys.stderr) + return 2 + agents_home = Path(config.get("agents_home", "~/.agents")).expanduser() + if not agents_home.is_absolute(): + agents_home = (context["project_root"] / agents_home).resolve() skills_src_root = PLAYBOOK_ROOT / "codex/skills" if not skills_src_root.is_dir(): print(f"ERROR: skills source not found: {skills_src_root}", file=sys.stderr) return 2 - skills_dst_root = codex_home / "skills" + skills_dst_root = agents_home / "skills" ensure_dir(skills_dst_root) if mode == "all": diff --git a/docs/standards/playbook/templates/AGENT_RULES.template.md b/docs/standards/playbook/templates/AGENT_RULES.template.md index a38bc86..a92d8b0 100644 --- a/docs/standards/playbook/templates/AGENT_RULES.template.md +++ b/docs/standards/playbook/templates/AGENT_RULES.template.md @@ -151,6 +151,15 @@ - **小步快跑**:每个 Plan 应该可快速完成 - **可验证**:每个 Plan 必须包含验证步骤 +## 复利工程 + +每次 Session 结束时: + +- **同一错误发生 2 次以上** → 立即更新 `AGENT_RULES.local.md` 或 `memory-bank/decisions.md`,避免下次重蹈 +- **发现项目特有规律**(如特定模块的注意事项、常见陷阱)→ 沉淀到 `AGENT_RULES.local.md` + +> 目标:让每次 Session 的起点比上次更高。 + ## 执行约束 ### 代码修改 @@ -180,6 +189,16 @@ - **避免循环**:避免重复调用同一工具获取相同信息 - **优先专用工具**:文件操作用 Read/Edit/Write,搜索用 Grep/Glob +## Context 管理 + +以下情况应建议用户**开启新 Session**: + +- 当前方向明显跑偏,需要从头重新理解需求 +- 讨论阶段产生了多个候选方案,进入执行阶段时应清空对话 +- Session 过长导致注意力涣散,重复犯同类错误 + +> 新 Session 在干净 context 下工作效果更好;切换不是失败,是重置起点。 + ## 需要确认的场景 **常规模式**(可交互): diff --git a/docs/standards/playbook/templates/README.md b/docs/standards/playbook/templates/README.md index 548252d..114626f 100644 --- a/docs/standards/playbook/templates/README.md +++ b/docs/standards/playbook/templates/README.md @@ -21,7 +21,8 @@ templates/ │ │ └── agent-behavior.template.md │ ├── coding/ │ │ ├── clarify.template.md -│ │ └── review.template.md +│ │ ├── review.template.md +│ │ └── code-review.template.md │ └── meta/ │ └── prompt-generator.template.md ├── ci/ # CI 模板 @@ -214,6 +215,7 @@ project/ | `system/agent-behavior.template.md` | 工作模式参考 | 切换探索/开发/调试模式 | | `coding/clarify.template.md` | 需求澄清模板 | 需求不明确时 | | `coding/review.template.md` | 复盘总结模板 | Plan 完成后复盘 | +| `coding/code-review.template.md` | 代码评审流程 | 执行 MR/PR 代码评审 | | `meta/prompt-generator.template.md` | 元提示词生成器 | 创建新的专用提示词 | ### AGENT_RULES.template.md diff --git a/docs/standards/playbook/templates/prompts/README.md b/docs/standards/playbook/templates/prompts/README.md index 0c681c1..c8cc1e1 100644 --- a/docs/standards/playbook/templates/prompts/README.md +++ b/docs/standards/playbook/templates/prompts/README.md @@ -11,7 +11,8 @@ prompts/ │ └── agent-behavior.md # 工作模式参考 ├── coding/ │ ├── clarify.md # 需求澄清模板 -│ └── review.md # 复盘总结模板 +│ ├── review.md # 复盘总结模板 +│ └── code-review.md # MR/PR 代码评审流程 └── meta/ └── prompt-generator.md # 元提示词生成器 ``` @@ -23,6 +24,7 @@ prompts/ | **agent-behavior.md** | 切换工作模式(探索/开发/调试) | | **clarify.md** | 需求不明确时澄清 | | **review.md** | Plan 完成后复盘总结 | +| **code-review.md** | 执行 MR/PR 代码评审 | | **prompt-generator.md** | 创建新的专用提示词 | ## 工作流程 @@ -36,6 +38,8 @@ prompts/ ↓ 执行计划 → AGENT_RULES 主循环(留痕) ↓ +代码评审(有 MR/PR 时)→ code-review.md + ↓ 完成复盘 → review.md ↓ 沉淀提示词 → prompt-generator.md(可选) diff --git a/docs/standards/playbook/templates/prompts/coding/code-review.template.md b/docs/standards/playbook/templates/prompts/coding/code-review.template.md new file mode 100644 index 0000000..8c79dc4 --- /dev/null +++ b/docs/standards/playbook/templates/prompts/coding/code-review.template.md @@ -0,0 +1,81 @@ +# Code Review 流程 + +## 触发场景 + +收到 MR/PR 需要评审时。 + +## 准备 + +切换到对应分支并获取变更内容: + +```bash +# GitLab +glab mr checkout +glab mr view | cat +glab mr diff | cat + +# GitHub +gh pr checkout +gh pr view +gh pr diff +``` + +## 评审流程 + +逐步执行以下维度。改动简单时可跳过某些步骤。 + +### 1. 理解业务目标 + +- 能否理解本次改动的业务目标? +- 如果目标不明确,先确认再评审。 + +### 2. High-level Review + +- 改动是否放在了合适的位置? +- 是否尽可能复用已有实现? +- 是否有破坏现有设计与逻辑的可能? + +### 3. Bug 检查 + +- 是否隐含业务错误、逻辑纰漏或安全问题? +- **未修改**的相关联代码是否有遗漏? + +### 4. 代码清晰度 + +- 逻辑是否简洁易懂? +- 命名是否清晰合理? +- 一年后再读,是否能轻松理解? + +### 5. KISS 原则 + +- 是否有不必要的复杂度? +- 是否有未使用的定义、过多参数? +- 是否重复造轮子? + +### 6. 单一职责 + +- 每个函数/类是否只做一件事? +- 文件/类/方法行数是否合理? + +### 7. 测试覆盖 + +- 复杂业务逻辑(含 if/else/for)是否有测试? +- 测试是否有效(非空实现)? +- 不应过度测试无控制逻辑的代码。 + +## 输出 + +评审完成后,总结发现的**重点问题**,按严重性排列。 + +## AI 与人工的分工 + +| 维度 | 负责方 | 说明 | +| ---- | ------ | ---- | +| Bug、逻辑漏洞、安全问题 | **AI + 人工** | AI 负责初筛与证据收集,结论需人工复核 | +| 代码清晰度、KISS、单一职责 | **AI + 人工** | AI 提供候选问题,人工决定是否采纳 | +| 架构合理性、业务对齐 | **人工** | AI 反馈少且准确率低,需人工把关 | +| 兼容性、历史债务、战略取舍 | **人工** | 依赖背景知识,AI 难以判断 | + +> 规则:AI 结论必须附文件路径、行号或可复现依据;缺少证据时按待确认假设处理。 +> +> 注意:评审不只看 diff,需结合代码库整体上下文做评估。 diff --git a/docs/standards/playbook/tests/README.md b/docs/standards/playbook/tests/README.md index 7f25ef3..7fc17b9 100644 --- a/docs/standards/playbook/tests/README.md +++ b/docs/standards/playbook/tests/README.md @@ -11,10 +11,12 @@ tests/ │ └── test_playbook_cli.py # playbook.py 基础功能测试 ├── test_format_md_action.py # format_md 动作测试 ├── test_gitattributes_modes.py # gitattr_mode 行为测试 +├── test_no_backup_flags.py # no_backup 行为测试 ├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试 ├── test_vendor_snapshot_templates.py # vendor 快照模板完整性测试 ├── test_plan_progress_cli.py # plan_progress CLI 测试 ├── test_superpowers_list_sync.py # superpowers 列表一致性测试 +├── test_superpowers_workflows.py # superpowers 工作流配置校验 ├── test_sync_templates_placeholders.py # 占位符替换测试(sync_rules/sync_standards) ├── test_toml_edge_cases.py # TOML 解析边界测试 ├── templates/ # 模板验证测试 diff --git a/docs/standards/playbook/tests/cli/test_playbook_cli.py b/docs/standards/playbook/tests/cli/test_playbook_cli.py index d070fa9..bb605b0 100644 --- a/docs/standards/playbook/tests/cli/test_playbook_cli.py +++ b/docs/standards/playbook/tests/cli/test_playbook_cli.py @@ -126,6 +126,27 @@ langs = ["tsl"] self.assertEqual(block[bullet_idx - 1], "") def test_install_skills(self): + with tempfile.TemporaryDirectory() as tmp_dir: + target = Path(tmp_dir) / "agents" + config_body = f""" +[playbook] +project_root = "{tmp_dir}" + +[install_skills] +agents_home = "{target}" +mode = "list" +skills = ["brainstorming"] +""" + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text(config_body, encoding="utf-8") + + result = run_cli("-config", str(config_path)) + + skill_file = target / "skills/brainstorming/SKILL.md" + self.assertEqual(result.returncode, 0) + self.assertTrue(skill_file.is_file()) + + def test_install_skills_rejects_codex_home(self): with tempfile.TemporaryDirectory() as tmp_dir: target = Path(tmp_dir) / "codex" config_body = f""" @@ -142,9 +163,8 @@ skills = ["brainstorming"] result = run_cli("-config", str(config_path)) - skill_file = target / "skills/brainstorming/SKILL.md" - self.assertEqual(result.returncode, 0) - self.assertTrue(skill_file.is_file()) + self.assertNotEqual(result.returncode, 0) + self.assertIn("codex_home", result.stdout + result.stderr) if __name__ == "__main__": unittest.main() diff --git a/docs/standards/playbook/tests/integration/check_doc_links.sh b/docs/standards/playbook/tests/integration/check_doc_links.sh index 5b512a5..65dfa18 100644 --- a/docs/standards/playbook/tests/integration/check_doc_links.sh +++ b/docs/standards/playbook/tests/integration/check_doc_links.sh @@ -15,11 +15,8 @@ VALID_LINKS=0 BROKEN_LINKS=0 SKIPPED_LINKS=0 -BROKEN_LINKS_FILE="/tmp/broken_links.txt" -REPORT_FILE="/tmp/doc_links_report.txt" - -> "$BROKEN_LINKS_FILE" -> "$REPORT_FILE" +BROKEN_LINKS_FILE="$(mktemp "${TMPDIR:-/tmp}/broken_links.XXXXXX")" +REPORT_FILE="$(mktemp "${TMPDIR:-/tmp}/doc_links_report.XXXXXX")" echo "📁 Playbook 根目录: $PLAYBOOK_ROOT" echo "" diff --git a/docs/standards/playbook/tests/templates/validate_project_templates.sh b/docs/standards/playbook/tests/templates/validate_project_templates.sh index 9eea2ff..341ddb8 100644 --- a/docs/standards/playbook/tests/templates/validate_project_templates.sh +++ b/docs/standards/playbook/tests/templates/validate_project_templates.sh @@ -91,6 +91,7 @@ validate_file_exists "$PROMPTS_DIR/README.md" "prompts/README.md" validate_file_exists "$PROMPTS_DIR/system/agent-behavior.template.md" "prompts/system/agent-behavior.template.md" validate_file_exists "$PROMPTS_DIR/coding/clarify.template.md" "prompts/coding/clarify.template.md" validate_file_exists "$PROMPTS_DIR/coding/review.template.md" "prompts/coding/review.template.md" +validate_file_exists "$PROMPTS_DIR/coding/code-review.template.md" "prompts/coding/code-review.template.md" validate_file_exists "$PROMPTS_DIR/meta/prompt-generator.template.md" "prompts/meta/prompt-generator.template.md" echo "" diff --git a/docs/standards/playbook/tests/test_readme_language_lists.py b/docs/standards/playbook/tests/test_readme_language_lists.py new file mode 100644 index 0000000..fb56c0e --- /dev/null +++ b/docs/standards/playbook/tests/test_readme_language_lists.py @@ -0,0 +1,15 @@ +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +README = ROOT / "README.md" + + +class ReadmeLanguageListsTests(unittest.TestCase): + def test_rulesets_list_includes_typescript(self): + text = README.read_text(encoding="utf-8") + self.assertIn("- `rulesets/typescript/index.md`:TypeScript 核心约定", text) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/standards/playbook/tests/test_superpowers_workflows.py b/docs/standards/playbook/tests/test_superpowers_workflows.py new file mode 100644 index 0000000..2049341 --- /dev/null +++ b/docs/standards/playbook/tests/test_superpowers_workflows.py @@ -0,0 +1,56 @@ +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SYNC_WORKFLOW = ROOT / ".gitea" / "workflows" / "sync-superpowers.yml" +AUTO_UPDATE_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers.yml" +AUTO_UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_superpowers.sh" +SYNC_SCRIPT = ROOT / ".gitea" / "ci" / "sync_superpowers.sh" + + +class SuperpowersWorkflowTests(unittest.TestCase): + def test_sync_workflow_uses_manual_trigger(self): + text = SYNC_WORKFLOW.read_text(encoding="utf-8") + self.assertIn("workflow_dispatch:", text) + + def test_sync_workflow_runs_from_latest_main(self): + text = SYNC_WORKFLOW.read_text(encoding="utf-8") + self.assertIn('TARGET_BRANCH: "main"', text) + self.assertIn('git fetch origin "${{ env.TARGET_BRANCH }}"', text) + self.assertIn( + 'git checkout -B "${{ env.TARGET_BRANCH }}" "origin/${{ env.TARGET_BRANCH }}"', + text, + ) + + def test_auto_update_workflow_triggers_on_main_push(self): + text = AUTO_UPDATE_WORKFLOW.read_text(encoding="utf-8") + self.assertIn("push:", text) + self.assertIn("- main", text) + self.assertIn("workflow_dispatch:", text) + + def test_auto_update_workflow_runs_update_script(self): + text = AUTO_UPDATE_WORKFLOW.read_text(encoding="utf-8") + self.assertIn("bash .gitea/ci/update_thirdparty_superpowers.sh", text) + + def test_auto_update_script_targets_thirdparty_branch(self): + text = AUTO_UPDATE_SCRIPT.read_text(encoding="utf-8") + self.assertIn('TARGET_BRANCH="${TARGET_BRANCH:-thirdparty/skill}"', text) + self.assertIn("api.github.com/repos", text) + self.assertIn("ls-remote", text) + self.assertIn('git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"', text) + + def test_ci_scripts_use_ci_bot_identity(self): + sync_text = SYNC_SCRIPT.read_text(encoding="utf-8") + update_text = AUTO_UPDATE_SCRIPT.read_text(encoding="utf-8") + + self.assertIn('COMMIT_AUTHOR_NAME="${COMMIT_AUTHOR_NAME:-ci-bot}"', sync_text) + self.assertIn('COMMIT_AUTHOR_EMAIL="${COMMIT_AUTHOR_EMAIL:-ci-bot@local}"', sync_text) + self.assertIn('COMMIT_AUTHOR_NAME="${COMMIT_AUTHOR_NAME:-ci-bot}"', update_text) + self.assertIn( + 'COMMIT_AUTHOR_EMAIL="${COMMIT_AUTHOR_EMAIL:-ci-bot@local}"', + update_text, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/standards/playbook/tests/test_sync_templates_placeholders.py b/docs/standards/playbook/tests/test_sync_templates_placeholders.py index f39c67e..9d6178f 100644 --- a/docs/standards/playbook/tests/test_sync_templates_placeholders.py +++ b/docs/standards/playbook/tests/test_sync_templates_placeholders.py @@ -16,6 +16,15 @@ def run_cli(*args): ) +def run_script(script_path: Path, *args, cwd: Path | None = None): + return subprocess.run( + [sys.executable, str(script_path), *args], + capture_output=True, + text=True, + cwd=str(cwd) if cwd else None, + ) + + class SyncTemplatesPlaceholdersTests(unittest.TestCase): def test_main_language_placeholder_replaced(self): with tempfile.TemporaryDirectory() as tmp_dir: @@ -44,6 +53,49 @@ langs = [\"cpp\", \"tsl\"] self.assertIn("docs/standards/playbook/scripts/plan_progress.py", rules_text) self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text) + def test_sync_standards_rewrites_typescript_docs_prefix_for_vendored_playbook(self): + with tempfile.TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + vendor_config = root / "vendor.toml" + vendor_config.write_text( + f""" +[playbook] +project_root = "{tmp_dir}" + +[vendor] +langs = ["typescript"] +""", + encoding="utf-8", + ) + + vendor_result = run_cli("-config", str(vendor_config)) + self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stderr) + + sync_config = root / "sync.toml" + sync_config.write_text( + f""" +[playbook] +project_root = "{tmp_dir}" + +[sync_standards] +langs = ["typescript"] +""", + encoding="utf-8", + ) + + vendored_script = ( + root / "docs" / "standards" / "playbook" / "scripts" / "playbook.py" + ) + sync_result = run_script( + vendored_script, "-config", str(sync_config), cwd=root + ) + self.assertEqual(sync_result.returncode, 0, msg=sync_result.stderr) + + agents_index = root / ".agents" / "typescript" / "index.md" + text = agents_index.read_text(encoding="utf-8") + self.assertIn("`docs/standards/playbook/docs/typescript/", text) + self.assertNotIn("`docs/typescript/", text) + if __name__ == "__main__": unittest.main()