diff --git a/.agents/skills/analyze-issue/SKILL.md b/.agents/skills/analyze-issue/SKILL.md index a638977..e37ca74 100644 --- a/.agents/skills/analyze-issue/SKILL.md +++ b/.agents/skills/analyze-issue/SKILL.md @@ -58,7 +58,7 @@ gh issue view --repo helloJamest/FinAgent --comments ### Step 5: 生成分析文档 -保存到 `.Codex/reviews/issues/issue-.md` +保存到 `.claude/reviews/issues/issue-.md` ## Output Document Format diff --git a/.agents/skills/analyze-pr/SKILL.md b/.agents/skills/analyze-pr/SKILL.md index 661a5d3..c121291 100644 --- a/.agents/skills/analyze-pr/SKILL.md +++ b/.agents/skills/analyze-pr/SKILL.md @@ -64,7 +64,7 @@ gh run view --log-failed ### Step 5: 生成评审文档 -保存到 `.Codex/reviews/prs/pr-.md` +保存到 `.claude/reviews/prs/pr-.md` ## Output Document Format diff --git a/.agents/skills/fix-issue/SKILL.md b/.agents/skills/fix-issue/SKILL.md index 1cebdfe..8233487 100644 --- a/.agents/skills/fix-issue/SKILL.md +++ b/.agents/skills/fix-issue/SKILL.md @@ -18,7 +18,7 @@ ### Step 1: 确认分析基线 -检查 `.Codex/reviews/issues/issue-.md` 是否存在;如果不存在,先补做 issue 分析或在本次修复中补齐最小分析结论。 +检查 `.claude/reviews/issues/issue-.md` 是否存在;如果不存在,先补做 issue 分析或在本次修复中补齐最小分析结论。 ### Step 2: 选择安全的工作方式 @@ -50,7 +50,7 @@ ### Step 5: 更新 issue 分析文档 -在 `.Codex/reviews/issues/issue-.md` 中补充: +在 `.claude/reviews/issues/issue-.md` 中补充: ```markdown ## Fix Implementation diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..14ff8a5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ +## PR Type + +- [ ] 新功能 +- [ ] 改进 +- [ ] 修复 +- [ ] 文档 +- [ ] 测试 +- [ ] chore + +## Background And Problem + + + +## Scope Of Change + + + +## Issue Link + + + +## Verification Commands And Results + + + +## Compatibility And Risk + + + +## Rollback Plan + + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ff89406 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,8 @@ +# Repository Instructions For Copilot And Coding Agents + +Canonical source: `AGENTS.md`. + +`CLAUDE.md` is a compatibility entry that must point to `AGENTS.md`. +Repository skills live in `.claude/skills/` and are mirrored in `.agents/skills/`. + +If any instruction here conflicts with `AGENTS.md`, follow `AGENTS.md`. diff --git a/.github/instructions/backend.instructions.md b/.github/instructions/backend.instructions.md new file mode 100644 index 0000000..54bb904 --- /dev/null +++ b/.github/instructions/backend.instructions.md @@ -0,0 +1,9 @@ +# Backend Instructions + +Canonical source: `AGENTS.md`. + +- Put backend changes in `src/`, `data_provider/`, `api/`, or `bot/`. +- Prefer existing services, repositories, schemas, and scripts. +- Validate Python backend changes with `bash scripts/ci_gate.sh` when practical. +- For small Python edits, at minimum run `python -m py_compile `. +- Keep data-source fallback and notification failure paths fail-open unless the task explicitly says otherwise. diff --git a/.github/instructions/client.instructions.md b/.github/instructions/client.instructions.md new file mode 100644 index 0000000..1b670b9 --- /dev/null +++ b/.github/instructions/client.instructions.md @@ -0,0 +1,9 @@ +# Client Instructions + +Canonical source: `AGENTS.md`. + +- Put Web changes in `apps/finagent-web/`. +- Put desktop changes in `apps/finagent-desktop/`. +- For Web changes, run `npm ci`, `npm run lint`, and `npm run build` from `apps/finagent-web/`. +- For desktop changes, build the Web app first, then run the desktop build. +- Keep API/schema changes backward compatible where possible. diff --git a/.github/instructions/governance.instructions.md b/.github/instructions/governance.instructions.md new file mode 100644 index 0000000..642e309 --- /dev/null +++ b/.github/instructions/governance.instructions.md @@ -0,0 +1,9 @@ +# Governance Instructions + +Canonical source: `AGENTS.md`. + +- Do not commit, tag, or push without explicit user confirmation. +- Keep `.env.example` and relevant docs in sync with new configuration. +- Update `docs/CHANGELOG.md` for user-visible, CLI/API, deployment, workflow, notification, or report changes. +- Keep `[Unreleased]` flat: `- [类型] 描述`, with type one of `新功能`, `改进`, `修复`, `文档`, `测试`, or `chore`. +- Run `python scripts/check_ai_assets.py` after changing AI collaboration assets. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b46fa01 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + ai-governance: + name: AI asset governance + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Check AI collaboration assets + run: python scripts/check_ai_assets.py + + backend-gate: + name: Backend gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: | + requirements.txt + requirements-ci.txt + + - name: Install backend CI dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-ci.txt + + - name: Run backend gate + run: bash scripts/ci_gate.sh + + docker-build: + name: Docker build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -f docker/Dockerfile -t finagent-ci . + + - name: Smoke import key modules + run: docker run --rm finagent-ci python -c "import api.app; import src.core.pipeline; import data_provider.base" + + web-gate: + name: Web gate + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/finagent-web + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: apps/finagent-web/package-lock.json + + - name: Install web dependencies + run: npm ci + + - name: Lint web + run: npm run lint + + - name: Build web + run: npm run build diff --git a/.github/workflows/network-smoke.yml b/.github/workflows/network-smoke.yml new file mode 100644 index 0000000..d76397d --- /dev/null +++ b/.github/workflows/network-smoke.yml @@ -0,0 +1,37 @@ +name: Network Smoke + +on: + schedule: + - cron: "0 2 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + network-smoke: + name: Network smoke + runs-on: ubuntu-latest + continue-on-error: true + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: | + requirements.txt + requirements-ci.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-ci.txt + + - name: Run network-marked pytest + run: python -m pytest -m network + + - name: Run quick deterministic smoke + run: bash test.sh quick --dry-run --no-notify diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml new file mode 100644 index 0000000..df2581b --- /dev/null +++ b/.github/workflows/pr-review.yml @@ -0,0 +1,38 @@ +name: PR Review + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + +jobs: + pr-static-checks: + name: PR static checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Check AI assets + run: python scripts/check_ai_assets.py + + - name: Check PR template exists + run: test -f .github/PULL_REQUEST_TEMPLATE.md + + ai-review-hook: + name: Optional AI review hook + runs-on: ubuntu-latest + continue-on-error: ${{ vars.AI_REVIEW_STRICT != 'true' }} + if: ${{ vars.AI_REVIEW_ENABLED == 'true' }} + steps: + - name: No reviewer configured + run: | + echo "AI_REVIEW_ENABLED is true, but no repository AI reviewer command is configured." + echo "Keep this job non-blocking unless AI_REVIEW_STRICT=true is intentionally enabled." diff --git a/AGENTS.md b/AGENTS.md index 26d6c5e..fcae906 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,9 +29,9 @@ - `AGENTS.md` 是仓库内 AI 协作规则的唯一真源。 - `CLAUDE.md` 必须是指向 `AGENTS.md` 的软链接,用于兼容 Claude 生态。 - `.github/copilot-instructions.md` 与 `.github/instructions/*.instructions.md` 是 GitHub Copilot / Coding Agent 的镜像或分层补充;若与本文件冲突,以 `AGENTS.md` 为准。 -- 仓库协作 skill 存放在 `.claude/skills/`,分析产物存放在 `.claude/reviews/`;前者可以入库,后者默认视为本地产物。 +- 仓库协作 skill 存放在 `.claude/skills/`,`.agents/skills/` 是同义镜像;分析产物存放在 `.claude/reviews/`,默认视为本地产物。 - 根目录 `SKILL.md` 与 `docs/openclaw-skill-integration.md` 属于产品或外部集成说明,不是仓库协作规则真源。 -- 若未来新增 `.agents/skills/` 或其他 agent 专用目录,必须先明确单一真源,再通过脚本或镜像同步;禁止手工长期维护多份同义内容。 +- 若未来新增其他 agent 专用目录,必须先明确单一真源,再通过脚本或镜像同步;禁止手工长期维护多份同义内容。 - 修改 AI 协作治理资产时,执行: ```bash diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 9e713e0..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,27 +0,0 @@ -AGENTS.md - -## gstack - -For all web browsing, use the `/browse` skill from gstack (`~/.claude/skills/gstack`). **Never** use `mcp__claude-in-chrome__*` tools. - -Available gstack skills: `/office-hours`, `/plan-ceo-review`, `/plan-eng-review`, `/plan-design-review`, `/design-consultation`, `/design-shotgun`, `/design-html`, `/review`, `/ship`, `/land-and-deploy`, `/canary`, `/benchmark`, `/browse`, `/connect-chrome`, `/qa`, `/qa-only`, `/design-review`, `/setup-browser-cookies`, `/setup-deploy`, `/retro`, `/investigate`, `/document-release`, `/codex`, `/cso`, `/autoplan`, `/plan-devex-review`, `/devex-review`, `/careful`, `/freeze`, `/guard`, `/unfreeze`, `/gstack-upgrade`, `/learn`. - -## Skill routing - -When the user's request matches an available skill, ALWAYS invoke it using the Skill -tool as your FIRST action. Do NOT answer directly, do NOT use other tools first. -The skill has specialized workflows that produce better results than ad-hoc answers. - -Key routing rules: -- Product ideas, "is this worth building", brainstorming -> invoke office-hours -- Bugs, errors, "why is this broken", 500 errors -> invoke investigate -- Ship, deploy, push, create PR -> invoke ship -- QA, test the site, find bugs -> invoke qa -- Code review, check my diff -> invoke review -- Update docs after shipping -> invoke document-release -- Weekly retro -> invoke retro -- Design system, brand -> invoke design-consultation -- Visual audit, polish -> invoke design-review -- Architecture review -> invoke plan-eng-review -- Save progress, checkpoint, resume -> invoke checkpoint -- Code quality, health check -> invoke health \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7078798..00c799d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,7 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - [修复] 补回 GitHub Actions 每日股票分析工作流,恢复 fork 后手动/定时运行能力(fixes #31)。 - [新功能] 打板策略(screen_board_play):通过 AKShare 获取龙虎榜、涨停池、连板天梯、涨停概念数据,自动筛选 1进2/2进3/3进4 晋级股,输出板块策略 + 连板预测 + 个股买入建议 - [改进] 打板策略结果在 UI 中按连板层级(1进2 / 2进3 / 3进4)分组展示,不同层级使用不同颜色区分 -- [新增] API 客户端新增 `getBoardData` 方法支持直接获取打板策略原始数据 +- [新功能] API 客户端新增 `getBoardData` 方法支持直接获取打板策略原始数据 +- [chore] 恢复 CI、网络冒烟与 PR 静态检查工作流,并补齐 GitHub Copilot / Coding Agent 协作资产镜像 +- [测试] 新增 AI 资产、CI 工作流与 Changelog `[Unreleased]` 格式守卫,防止协作治理漂移 - [修复] `AGENT_MAX_STEPS` 在 orchestrator 多 Agent 模式下改为作为各子 Agent 的步数上限而非硬覆盖;TechnicalAgent 等高默认值 Agent 会被封顶,低默认值 Agent 保持原值,减少不必要的 LLM 调用膨胀与配额消耗。 - [修复] **MiniMax-M2.7 模型连接测试支持** — 修复 LLM 通道连接测试在 MiniMax-M2.7 模型下返回 "Empty response" 的问题;增加了 `max_tokens` 上限(8→256)以容纳 MiniMax 思考过程,并添加 `content_blocks` 格式解析逻辑统一处理 MiniMax 响应格式差异。 - [修复] 移除 `HistoryItem` 与 `ReportSummary` 响应 Schema 中 `sentiment_score` 的 `ge=0/le=100` 约束(fixes #942)——历史库中存储的超范围负值或大于 100 的情绪评分不再触发 Pydantic ValidationError,历史列表与详情接口恢复正常返回。 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d4fd45a..b9e08f6 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -78,10 +78,12 @@ docs: 更新 README 部署说明 | 检查项 | 说明 | 必须通过 | |--------|------|:--------:| +| ai-governance | 校验 `AGENTS.md` / `CLAUDE.md` / `.github` 指令 / repository skills 关系 | ✅ | | backend-gate | `scripts/ci_gate.sh`(py_compile + flake8 严重错误 + 本地核心脚本 + offline pytest) | ✅ | | docker-build | Docker 镜像构建与关键模块导入 smoke | ✅ | | web-gate | 前端变更时执行 `npm run lint` + `npm run build` | ✅(触发时) | | network-smoke | 定时/手动执行 `pytest -m network` + `test.sh quick`(非阻断) | ❌(观测项) | +| pr-review | PR 模板与协作资产静态检查,AI review hook 可选启用 | ❌(辅助项) | **本地运行检查:** diff --git a/docs/CONTRIBUTING_EN.md b/docs/CONTRIBUTING_EN.md index 5075458..c705b86 100644 --- a/docs/CONTRIBUTING_EN.md +++ b/docs/CONTRIBUTING_EN.md @@ -80,9 +80,11 @@ After opening a PR, CI will automatically run the following PR checks: | Check | Description | Required | |-------|-------------|:--------:| +| `ai-governance` | Checks `AGENTS.md` / `CLAUDE.md` / `.github` instructions / repository skills alignment | ✅ | | `backend-gate` | `scripts/ci_gate.sh` — py_compile + flake8 critical errors + `./test.sh code` + `./test.sh yfinance` + offline pytest | ✅ | | `docker-build` | Docker image build and key module import smoke test | ✅ | | `web-gate` | `npm run lint` + `npm run build` (triggered when `apps/finagent-web/` changes) | ✅ (when triggered) | +| `pr-review` | PR template and collaboration asset static checks; optional AI review hook | ❌ (auxiliary) | Separately, the repository also has a non-blocking `network-smoke` workflow in `.github/workflows/network-smoke.yml`, but it is only triggered by `schedule` and `workflow_dispatch`, not by pull requests. diff --git a/scripts/check_ai_assets.py b/scripts/check_ai_assets.py index fa68f46..0152e86 100644 --- a/scripts/check_ai_assets.py +++ b/scripts/check_ai_assets.py @@ -13,6 +13,7 @@ COPILOT = ROOT / ".github" / "copilot-instructions.md" INSTRUCTIONS_DIR = ROOT / ".github" / "instructions" CLAUDE_SKILLS_DIR = ROOT / ".claude" / "skills" +AGENTS_SKILLS_DIR = ROOT / ".agents" / "skills" REQUIRED_INSTRUCTION_FILES = { "backend.instructions.md", @@ -39,6 +40,13 @@ def fail(message: str) -> None: sys.exit(1) +def normalize_windows_long_path(path: Path) -> Path: + text = str(path) + if text.startswith("\\\\?\\"): + return Path(text[4:]) + return path + + def ensure_file_exists(path: Path, description: str) -> None: if not path.exists(): fail(f"{description} is missing: {path.relative_to(ROOT)}") @@ -51,7 +59,13 @@ def ensure_symlink() -> None: if not CLAUDE.is_symlink(): fail("CLAUDE.md must be a symlink to AGENTS.md") - target = Path(CLAUDE.readlink()) + target = normalize_windows_long_path(Path(CLAUDE.readlink())) + if target == Path("AGENTS.md"): + return + if target.is_absolute() and target.resolve() == AGENTS.resolve(): + return + if not target.is_absolute() and (CLAUDE.parent / target).resolve() == AGENTS.resolve(): + return if target != Path("AGENTS.md"): fail(f"CLAUDE.md must point to AGENTS.md, found: {target}") @@ -89,6 +103,15 @@ def ensure_skill_files() -> None: if relative_path != "README.md" and "AGENTS.md" not in content: fail(f"{path.relative_to(ROOT)} must reference AGENTS.md as the rule source") + ensure_file_exists(AGENTS_SKILLS_DIR, "agent skills mirror directory") + for relative_path in REQUIRED_SKILL_FILES - {"README.md"}: + claude_path = CLAUDE_SKILLS_DIR / relative_path + agents_path = AGENTS_SKILLS_DIR / relative_path + if not agents_path.exists(): + fail(f"missing agent skill mirror: {agents_path.relative_to(ROOT)}") + if agents_path.read_text(encoding="utf-8") != claude_path.read_text(encoding="utf-8"): + fail(f"agent skill mirror is out of sync: {agents_path.relative_to(ROOT)}") + def ensure_gitignore_rules() -> None: gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8") @@ -106,9 +129,10 @@ def ensure_no_tracked_claude_artifacts() -> None: check=True, ) tracked = [line.strip() for line in result.stdout.splitlines() if line.strip()] - allowed_prefixes = (".claude/skills/",) + allowed_prefixes = (".claude/skills/", ".claude/hooks/") + allowed_files = {".claude/settings.json"} for path in tracked: - if path.startswith(allowed_prefixes): + if path in allowed_files or path.startswith(allowed_prefixes): continue fail(f"tracked .claude artifact outside skills/: {path}") diff --git a/tests/test_ai_assets_contract.py b/tests/test_ai_assets_contract.py new file mode 100644 index 0000000..a4bf1cd --- /dev/null +++ b/tests/test_ai_assets_contract.py @@ -0,0 +1,17 @@ +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_ai_asset_governance_script_passes(): + result = subprocess.run( + [sys.executable, "scripts/check_ai_assets.py"], + cwd=ROOT, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr or result.stdout diff --git a/tests/test_changelog_unreleased_format.py b/tests/test_changelog_unreleased_format.py new file mode 100644 index 0000000..d7b5b4d --- /dev/null +++ b/tests/test_changelog_unreleased_format.py @@ -0,0 +1,33 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +CHANGELOG = ROOT / "docs" / "CHANGELOG.md" +ALLOWED_TYPES = {"新功能", "改进", "修复", "文档", "测试", "chore"} + + +def _unreleased_lines() -> list[str]: + text = CHANGELOG.read_text(encoding="utf-8") + start = text.index("## [Unreleased]") + rest = text[start:].splitlines()[1:] + lines: list[str] = [] + for line in rest: + if line.startswith("## ["): + break + lines.append(line) + return lines + + +def test_unreleased_changelog_uses_flat_allowed_types(): + entries = [line for line in _unreleased_lines() if line.startswith("- [")] + + assert entries + for entry in entries: + entry_type = entry.split("]", 1)[0].removeprefix("- [") + assert entry_type in ALLOWED_TYPES, entry + + +def test_unreleased_changelog_has_no_category_headings(): + headings = [line for line in _unreleased_lines() if line.startswith("### ")] + + assert headings == [] diff --git a/tests/test_github_actions_workflow.py b/tests/test_github_actions_workflow.py index 2b95c37..3f380b3 100644 --- a/tests/test_github_actions_workflow.py +++ b/tests/test_github_actions_workflow.py @@ -3,6 +3,8 @@ ROOT = Path(__file__).resolve().parents[1] DAILY_ANALYSIS_WORKFLOW = ROOT / ".github" / "workflows" / "daily_analysis.yml" +CI_WORKFLOW = ROOT / ".github" / "workflows" / "ci.yml" +README = ROOT / "README.md" def test_daily_analysis_workflow_is_present_and_triggerable(): @@ -17,3 +19,24 @@ def test_daily_analysis_workflow_is_present_and_triggerable(): assert "GEMINI_API_KEY:" in text assert "OPENAI_API_KEY:" in text assert "--force-run" in text + + +def test_ci_workflow_matches_documented_required_gates(): + assert CI_WORKFLOW.exists() + + text = CI_WORKFLOW.read_text(encoding="utf-8") + + assert "ai-governance" in text + assert "backend-gate" in text + assert "docker-build" in text + assert "web-gate" in text + assert "scripts/ci_gate.sh" in text + assert "npm run lint" in text + assert "npm run build" in text + + +def test_readme_ci_badge_points_to_existing_workflow(): + readme = README.read_text(encoding="utf-8") + + assert "actions/workflows/ci.yml" in readme + assert CI_WORKFLOW.exists()