Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .codex/schemas/team-collaboration.schema.json
Original file line number Diff line number Diff line change
@@ -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"]
}
23 changes: 23 additions & 0 deletions .codex/scripts/doctor_team_collaboration.py
Original file line number Diff line number Diff line change
@@ -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())
144 changes: 144 additions & 0 deletions .codex/scripts/validate_pr_contract.py
Original file line number Diff line number Diff line change
@@ -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())
68 changes: 68 additions & 0 deletions .codex/scripts/validate_team_collaboration.py
Original file line number Diff line number Diff line change
@@ -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"<!-- TEAM-COLLABORATION:START(?: [^>]*)? -->\n.*?\n<!-- TEAM-COLLABORATION:END -->\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())
71 changes: 71 additions & 0 deletions .codex/team-collaboration.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
57 changes: 57 additions & 0 deletions .github/ISSUE_TEMPLATE/critical-goal.yml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading