diff --git a/.codex/schemas/team-collaboration.schema.json b/.codex/schemas/team-collaboration.schema.json new file mode 100644 index 0000000..0501b46 --- /dev/null +++ b/.codex/schemas/team-collaboration.schema.json @@ -0,0 +1,6 @@ +{ + "_generated": "Generated by agents-team", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["mechanism", "project", "commands", "paths", "risk", "overrides", "enforcement", "managedFiles"] +} diff --git a/.codex/scripts/doctor_team_collaboration.py b/.codex/scripts/doctor_team_collaboration.py new file mode 100644 index 0000000..f76e030 --- /dev/null +++ b/.codex/scripts/doctor_team_collaboration.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Generated by agents-team. Run a read-only adapter health check.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def main() -> int: + root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() + validator = Path(__file__).with_name("validate_team_collaboration.py") + result = subprocess.run([sys.executable, str(validator), str(root)], text=True) + if result.returncode == 0: + print("doctor: healthy") + else: + print("doctor: blocked") + return result.returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.codex/scripts/validate_pr_contract.py b/.codex/scripts/validate_pr_contract.py new file mode 100644 index 0000000..0647efc --- /dev/null +++ b/.codex/scripts/validate_pr_contract.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Generated by agents-team. Validate a PR, its Goal Issue, and current-head evidence.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import urllib.error +import urllib.request +from datetime import datetime +from pathlib import Path + + +PR_SECTIONS = [ + "关联任务", "风险等级", "实际改动", "范围偏差", "必须完成项证据", + "测试门禁", "行为验收", "QA 独立性与结论", "剩余风险", "回滚方式", "失败记录", +] +ISSUE_SECTIONS = ["Goal", "必须完成", "验收门禁", "任务边界", "风险等级", "依赖与阻塞条件", "失败处理与回滚"] +HEADING = re.compile(r"^#{2,6}\s+(.+?)\s*$", re.MULTILINE) +ISSUE_LINK = re.compile(r"\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b", re.IGNORECASE) +PLACEHOLDER = re.compile(r"<[^>]+>|待验收|待补充|TODO", re.IGNORECASE) + + +def sections(body: str) -> dict[str, str]: + matches = list(HEADING.finditer(body or "")) + result: dict[str, str] = {} + for index, match in enumerate(matches): + end = matches[index + 1].start() if index + 1 < len(matches) else len(body) + result[match.group(1).strip()] = body[match.end():end].strip() + return result + + +def scalar(block: str, name: str) -> str: + match = re.search(rf"(?mi)^\s*{re.escape(name)}\s*:\s*(.+?)\s*$", block) + return match.group(1).strip() if match else "" + + +def valid_timestamp(value: str) -> bool: + try: + datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return False + return bool(value) + + +def validate(pr_body: str, issue_body: str, head_sha: str) -> list[str]: + errors: list[str] = [] + pr = sections(pr_body) + issue = sections(issue_body) + for name in PR_SECTIONS: + if not pr.get(name, "").strip(): + errors.append(f"PR missing section: {name}") + for name in ISSUE_SECTIONS: + if not issue.get(name, "").strip(): + errors.append(f"Issue missing section: {name}") + if not ISSUE_LINK.search(pr_body or ""): + errors.append("PR must close a numbered Issue") + if PLACEHOLDER.search(pr_body or ""): + errors.append("PR contains unresolved placeholders") + + tests = pr.get("测试门禁", "") + for name in ("gate", "command", "exitCode", "passed", "failed", "skipped", "timestamp", "commitSha", "artifact"): + if not scalar(tests, name): + errors.append(f"test evidence missing: {name}") + if scalar(tests, "exitCode") != "0" or scalar(tests, "failed") != "0": + errors.append("test evidence is not successful") + commit_sha = scalar(tests, "commitSha") + if commit_sha and commit_sha != head_sha: + errors.append(f"test evidence commitSha {commit_sha} does not match head.sha {head_sha}") + timestamp = scalar(tests, "timestamp") + if timestamp and not valid_timestamp(timestamp): + errors.append("test evidence timestamp is not ISO 8601") + artifact = scalar(tests, "artifact") + if artifact and not re.match(r"https://", artifact): + errors.append("test evidence artifact must be an HTTPS URL") + + risk = pr.get("风险等级", "") + "\n" + issue.get("风险等级", "") + if re.search(r"\bL[23]\b", risk, re.IGNORECASE): + qa = pr.get("QA 独立性与结论", "") + if not re.search(r"独立上下文\s*[::]\s*是", qa): + errors.append("L2/L3 QA must use an independent context") + if not re.search(r"结论\s*[::]\s*PASS\b", qa, re.IGNORECASE): + errors.append("L2/L3 QA verdict must be PASS") + return errors + + +def fetch_issue(repo: str, number: int, token: str) -> str: + request = urllib.request.Request( + f"https://api.github.com/repos/{repo}/issues/{number}", + headers={"Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", "User-Agent": "agents-team-gate"}, + ) + try: + with urllib.request.urlopen(request, timeout=20) as response: + return str(json.loads(response.read().decode("utf-8")).get("body") or "") + except (urllib.error.HTTPError, urllib.error.URLError, json.JSONDecodeError) as exc: + raise RuntimeError(f"cannot read linked Issue: {exc}") from exc + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--event", type=Path, required=True) + parser.add_argument("--issue-body-file", type=Path) + args = parser.parse_args() + event = json.loads(args.event.read_text(encoding="utf-8")) + token = os.environ.get("GITHUB_TOKEN", "") + pull_request = event.get("pull_request") or {} + if pull_request.get("draft"): + print("draft PR: strict contract deferred until ready for review") + return 0 + + pr_body = str(pull_request.get("body") or "") + head_sha = str((pull_request.get("head") or {}).get("sha") or "") # head.sha + repo = str((event.get("repository") or {}).get("full_name") or os.environ.get("GITHUB_REPOSITORY") or "") + linked = ISSUE_LINK.search(pr_body) + if not head_sha or not repo or linked is None: + errors = ["event is missing repository, head.sha, or linked Issue"] + else: + if args.issue_body_file: + issue_body = args.issue_body_file.read_text(encoding="utf-8") + else: + if not token: + print("Agents-Team gate blocked: GITHUB_TOKEN is required") + return 1 + try: + issue_body = fetch_issue(repo, int(linked.group(1)), token) + except RuntimeError as exc: + print(f"Agents-Team gate blocked: {exc}") + return 1 + errors = validate(pr_body, issue_body, head_sha) + + if errors: + print("Agents-Team PR contract failed:") + for error in errors: + print(f"- {error}") + return 1 + print("Agents-Team PR contract passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.codex/scripts/validate_team_collaboration.py b/.codex/scripts/validate_team_collaboration.py new file mode 100644 index 0000000..9479563 --- /dev/null +++ b/.codex/scripts/validate_team_collaboration.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Generated by agents-team. Self-contained project validator.""" + +import json +import hashlib +import re +import sys +from pathlib import Path + + +REQUIRED = [ + "AGENTS.md", + ".codex/team-collaboration.json", + ".codex/scripts/validate_pr_contract.py", + ".codex/scripts/doctor_team_collaboration.py", + ".github/ISSUE_TEMPLATE/team-goal.yml", + ".github/ISSUE_TEMPLATE/critical-goal.yml", + ".github/pull_request_template.md", +] +BLOCK_RE = re.compile( + r"\n.*?\n\n?", + re.DOTALL, +) + + +def sha256(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def main() -> int: + root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() + errors = [f"missing: {path}" for path in REQUIRED if not (root / path).is_file()] + config_path = root / ".codex/team-collaboration.json" + if config_path.is_file(): + try: + config = json.loads(config_path.read_text(encoding="utf-8")) + if config.get("mechanism", {}).get("id") != "agents-team": + errors.append("invalid mechanism.id") + for target, expected in config.get("managedFiles", {}).items(): + if target == "AGENTS.md#TEAM-COLLABORATION": + agents_path = root / "AGENTS.md" + matches = BLOCK_RE.findall(agents_path.read_text(encoding="utf-8")) if agents_path.is_file() else [] + if len(matches) != 1: + errors.append("missing or malformed AGENTS managed block") + continue + data = matches[0].encode("utf-8") + else: + managed_path = root / target + if not managed_path.is_file(): + errors.append(f"missing: {target}") + continue + data = managed_path.read_bytes() + if sha256(data) != expected: + errors.append(f"managed file drift: {target}") + except (OSError, json.JSONDecodeError) as exc: + errors.append(f"invalid config: {exc}") + agents = root / "AGENTS.md" + if agents.is_file() and "TEAM-COLLABORATION:START" not in agents.read_text(encoding="utf-8"): + errors.append("missing AGENTS managed block") + for error in errors: + print(error) + if not errors: + print("team collaboration project adapter: valid") + return 1 if errors else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.codex/team-collaboration.json b/.codex/team-collaboration.json new file mode 100644 index 0000000..a1c9dd5 --- /dev/null +++ b/.codex/team-collaboration.json @@ -0,0 +1,71 @@ +{ + "$schema": "./schemas/team-collaboration.schema.json", + "mechanism": { + "id": "agents-team", + "pluginVersion": "0.3.0", + "protocolVersion": "2.0.0", + "configSchemaVersion": 2, + "initializedAt": "2026-06-29T16:21:03Z", + "lastUpgradedAt": null + }, + "project": { + "name": "agents-team-skill-usage-pZ4IrA", + "type": "generic", + "repository": "DOIT-Ben/skill-usage", + "defaultBranch": "main" + }, + "commands": { + "test": "", + "build": "", + "lint": "", + "typecheck": "", + "e2e": "" + }, + "paths": { + "frontend": [], + "backend": [], + "database": [], + "deployment": [], + "tests": [] + }, + "risk": { + "criticalPaths": [], + "protectedFiles": [], + "productionPaths": [], + "realProviderPaths": [] + }, + "overrides": { + "requireIssueForL1": false, + "requireRealSmokeForRelease": true + }, + "enforcement": { + "mode": "strict", + "failClosedRisks": [ + "L2", + "L3" + ], + "requireLinkedIssue": { + "L1": false, + "L2": true, + "L3": true + }, + "requireIndependentQA": { + "L1": false, + "L2": true, + "L3": true + }, + "requireFailureRecord": true + }, + "managedFiles": { + ".codex/schemas/team-collaboration.schema.json": "sha256:3df7800f6802146c496da6241dab8be0e97687c7fcc097a110e6ea9457de96e7", + ".codex/scripts/doctor_team_collaboration.py": "sha256:5026a93cc571e8ef3ee16ab83b1ab77e026d040d0894332d2ead5f08a203b084", + ".codex/scripts/validate_pr_contract.py": "sha256:8e58749e37725760174d5c1ecd64c9c240cb91aafe915fd279d89af4eafe3555", + ".codex/scripts/validate_team_collaboration.py": "sha256:9527cab3b26c5c9111850e125d8fec2a7414017201a8b42feabc2bff15926f25", + ".github/ISSUE_TEMPLATE/critical-goal.yml": "sha256:816e620235b01037a5b4009de8b2ff1d30c1ab66beb6e1b31bff2999545bf9d9", + ".github/ISSUE_TEMPLATE/team-goal.yml": "sha256:5708e9c0bdff27fc2fa2e26d6d8c782cb3d4c2249aac9be772b32c66faef5a44", + ".github/pull_request_template.md": "sha256:96953d71fedc2939255e89f9baaf42c79f5c51a70bfd67badaf95f2ece660eac", + ".github/workflows/collaboration-gate.yml": "sha256:a941f6c7094b4974133ddd2642d60be4f252edf2d534694a7db4bdf9f0e05e36", + "docs/adr/README.md": "sha256:eac61dad3e66bc2adf9fd75f0bd43e30d55a77a6d4a3957df474bf7f723a64ff", + "AGENTS.md#TEAM-COLLABORATION": "sha256:c82f5150a2d2a8aa1bb41ff7cbefa9469ab12329e96ea02a342ce0b0bb61f8dd" + } +} diff --git a/.github/ISSUE_TEMPLATE/critical-goal.yml b/.github/ISSUE_TEMPLATE/critical-goal.yml new file mode 100644 index 0000000..3418c10 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/critical-goal.yml @@ -0,0 +1,57 @@ +# Generated by agents-team +name: 关键团队目标 +description: 创建必须由用户确认方案的 L3 Goal +title: "[L3 Goal] " +labels: ["status:ready", "risk:L3", "decision-needed"] +body: + - type: textarea + id: goal + attributes: + label: Goal + validations: + required: true + - type: textarea + id: must_complete + attributes: + label: 必须完成 + validations: + required: true + - type: textarea + id: acceptance_gates + attributes: + label: 验收门禁 + validations: + required: true + - type: textarea + id: boundaries + attributes: + label: 任务边界 + validations: + required: true + - type: dropdown + id: risk + attributes: + label: 风险等级 + options: ["L3"] + validations: + required: true + - type: textarea + id: decision + attributes: + label: L3 方案与用户确认 + description: 记录方案、备选、成本、风险、回滚以及用户确认结论。 + validations: + required: true + - type: textarea + id: dependencies + attributes: + label: 依赖与阻塞条件 + validations: + required: true + - type: textarea + id: failure_rollback + attributes: + label: 失败处理与回滚 + description: 记录失败停止条件、证据保留、回滚步骤和回滚后的验证。 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/team-goal.yml b/.github/ISSUE_TEMPLATE/team-goal.yml new file mode 100644 index 0000000..6770704 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/team-goal.yml @@ -0,0 +1,55 @@ +# Generated by agents-team +name: 团队目标 +description: 创建需要实现与独立验收的标准 Goal +title: "[Goal] " +labels: ["status:ready", "risk:L2"] +body: + - type: textarea + id: goal + attributes: + label: Goal + description: 用一句话定义最终必须达到的可观察结果。 + validations: + required: true + - type: textarea + id: must_complete + attributes: + label: 必须完成 + description: 所有条目完成后才允许进入 QA,请使用任务清单。 + validations: + required: true + - type: textarea + id: acceptance_gates + attributes: + label: 验收门禁 + description: 写明必须通过的测试命令、行为路径和证据。 + validations: + required: true + - type: textarea + id: boundaries + attributes: + label: 任务边界 + description: 明确严禁修改、重构、读取、调用或扩展的内容。 + validations: + required: true + - type: dropdown + id: risk + attributes: + label: 风险等级 + options: ["L1", "L2", "L3"] + validations: + required: true + - type: textarea + id: dependencies + attributes: + label: 依赖与阻塞条件 + description: 写明环境、权限、数据和外部服务条件;没有外部依赖时说明可在何种本地条件下执行。 + validations: + required: true + - type: textarea + id: failure_rollback + attributes: + label: 失败处理与回滚 + description: 写明失败证据如何保留、何时停止以及如何恢复到安全状态。 + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..8f37044 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,62 @@ + +## 关联任务 + +Closes # + +## 风险等级 + +L1 / L2 / L3 + +## 实际改动 + +列出最终行为变化和涉及文件。 + +## 范围偏差 + +说明与 Issue 任务边界的偏差及确认记录;没有偏差时写明“无偏差”。 + +## 必须完成项证据 + +- [ ] 必须完成项:可检查的测试、行为或产物证据 + +## 测试门禁 + +```yaml +gate: test:unit +command: +exitCode: +passed: +failed: +skipped: +skipReason: +timestamp: +commitSha: +artifact: +``` + +## 行为验收 + +- 场景: +- 入口与步骤: +- 预期: +- 实际: +- 证据: + +## QA 独立性与结论 + +- 独立上下文:是 / 否 +- 验收者: +- 结论:PASS / FAIL / BLOCKED +- 证据: + +## 剩余风险 + +列出仍存在的风险及负责人。 + +## 回滚方式 + +写明回滚命令、数据恢复方式和回滚后验证。 + +## 失败记录 + +记录本任务出现过的失败、根因和回归证据;确实没有失败时写“无失败记录”。 diff --git a/.github/workflows/collaboration-gate.yml b/.github/workflows/collaboration-gate.yml new file mode 100644 index 0000000..db64140 --- /dev/null +++ b/.github/workflows/collaboration-gate.yml @@ -0,0 +1,25 @@ +# Generated by agents-team +name: Collaboration Gate + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, edited] + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + issues: read + +jobs: + validate-collaboration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate team collaboration project adapter + run: python .codex/scripts/validate_team_collaboration.py . + - name: Validate PR, linked Issue, and current-head evidence + if: github.event_name == 'pull_request' + env: + GITHUB_TOKEN: ${{ github.token }} + run: python .codex/scripts/validate_pr_contract.py --event "$GITHUB_EVENT_PATH" diff --git a/AGENTS.md b/AGENTS.md index d45e4a8..1adbbcc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,3 +115,19 @@ Only commit files in this repository. Do not commit: - Log files - Caches - User-specific paths + + +## 团队协作机制 + +本项目已启用 `agents-team`。开始开发任务前,读取 `.codex/team-collaboration.json`。 + +- 所有正式任务必须先有清晰、可观察的 Goal。 +- L2/L3 必须使用 GitHub Issue;Issue 必须依次包含 Goal、必须完成、验收门禁、任务边界、风险等级、依赖与阻塞条件。 +- “必须完成”缺一项、验收门禁未通过或任务边界被突破时,不得宣布完成。 +- L1 可由主 Codex 直接处理;L2 必须独立 QA;L3 实施前必须获得用户确认,实施后必须独立复核与 QA。 +- 执行智能体只能声明代码级完成,不得声明 QA PASS、可上线或发布完成。 +- 指定测试失败时必须修复实现,不得删除测试或用无解释的 skip/xfail 规避。 +- GitHub Issue 管承诺,Pull Request 管事实,QA 管结论;不得维护第二套动态任务台账。 +- 真实数据、权限、密钥、付费 Provider、不可逆操作和生产发布必须暂停并请求用户确认。 +- 所有改动必须遵守项目级测试命令、高风险路径和保护文件配置。 + diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..ae539ed --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,6 @@ + +# Architecture Decision Records + +仅为跨版本、难以撤销、影响多个模块或改变核心数据/API/状态机/Provider/部署路线的决定创建 ADR。 + +每份 ADR 包含:状态、日期、关联 Issue、背景、候选方案、决策、原因、后果和替代关系。 diff --git a/docs/agents-team-l2-smoke.md b/docs/agents-team-l2-smoke.md new file mode 100644 index 0000000..9f37985 --- /dev/null +++ b/docs/agents-team-l2-smoke.md @@ -0,0 +1,41 @@ +# Agents-Team L2 smoke trial + +This branch is an isolated trial of Agents-Team 0.3.0. It must not be merged +into `main` without a separate decision. + +## Scope + +- Generated the repository adapter, contracts, and Collaboration Gate. +- Did not modify the skill scanner, ranking rules, privacy behavior, or release. +- Used no real session data, credentials, paid providers, or production systems. + +## Initialization evidence + +- Command: `python3 plugins/agents-team/scripts/initialize_project.py --apply` +- Exit code: `0` +- Result: `11` adapter files created. +- Project profile: `generic`; the repository has no root Python project manifest. + +## Local validation evidence + +- Command: `python .codex/scripts/validate_team_collaboration.py .` +- Exit code: `0` +- Result: `team collaboration project adapter: valid` + +## Preflight failure + +- Symptom: the first projected adapter replaced the existing AGENTS.md and the + comparison showed 117 deleted lines. +- Root cause: initialization was run against an empty local projection rather + than a complete checkout, so existing project rules were absent. +- Correction: restored the complete existing AGENTS.md and appended only the + managed collaboration block. The final comparison has zero deletions. + +## GitHub gate evidence + +Pending. The trial first submits intentionally incomplete PR evidence to prove +that the gate fails closed, then replaces it with current-head evidence. + +## Rollback + +Close the trial pull request and delete `test/agents-team-l2-smoke`.