#!/bin/zsh set -euo pipefail ROOT=${0:A:h:h} cd "$ROOT" openspec doctor --json | grep -q '"healthy": true' openspec validate --all >/dev/null for forbidden in .planning .scratch .sisyphus CONTEXT.md specs; do [[ ! -e "$forbidden" ]] || { print -u2 "禁止目录或文件仍存在:$forbidden"; exit 1; } done [[ -z "$(find "$HOME/.codex/skills" -maxdepth 1 -name 'gsd-*' -print 2>/dev/null)" ]] [[ -z "$(find "$HOME/.codex/agents" -maxdepth 1 -name 'gsd-*' -print 2>/dev/null)" ]] [[ -z "$(command -v omx 2>/dev/null || true)" ]] ! grep -RIlE 'oh-my-codex|OMX:' AGENTS.md CLAUDE.md .agents .claude >/dev/null 2>&1 [[ -z "$(git ls-files .lh-harness)" ]] [[ -z "$(find . -type f -name '*_test.go' -not -path './.git/*' -not -path './.lh-harness/*' -print)" ]] [[ ! -e tests ]] [[ ! -e internal/testutil ]] [[ ! -e scripts/benchmark ]] [[ -z "$(find scripts -type f -name 'test_*' -print 2>/dev/null)" ]] python3 - <<'PY' from pathlib import Path import json, re, sys files=[Path('AGENTS.md'),Path('CLAUDE.md'),Path('README.md'),Path('ARCHITECTURE.md'),*Path('docs').rglob('*.md'),*Path('openspec').rglob('*.md')] bad=[] for p in files: for link in re.findall(r'\[[^]]*\]\(([^)]+)\)', p.read_text(errors='ignore')): target=link.split('#')[0] if not target or '://' in target or target.startswith('mailto:'): continue if not (p.parent/target).resolve().exists(): bad.append(f'{p}: {link}') if bad: print('失效链接:\n'+'\n'.join(bad), file=sys.stderr) raise SystemExit(1) spec_requirements=set() indexed_routes=set() for spec in Path('openspec/specs').glob('*/spec.md'): text=spec.read_text() if re.search(r'^### Requirement: .*接口集合$', text, re.M): raise SystemExit(f'接口集合仍被用作行为 Requirement:{spec}') names=re.findall(r'^### Requirement: (.+)$', text, re.M) if not names: raise SystemExit(f'Capability 缺少行为 Requirement:{spec}') spec_requirements.update(f'{spec.parent.name}::{name}' for name in names) indexed_routes.update(f'{method} {path}' for method,path in re.findall(r'`(GET|POST|PUT|PATCH|DELETE) ([^`]+)`', text)) evidence_dir=Path('.lh-harness/context-reset') evidence=json.loads((evidence_dir/'requirement-evidence.json').read_text()) matrix=json.loads((evidence_dir/'entry-capability-requirement-matrix.json').read_text()) evidence_requirements={f"{row['capability']}::{row['requirement']}" for row in evidence} if evidence_requirements != spec_requirements: raise SystemExit('Requirement 证据链与 Specs 不一致') matrix_http={row['entry'] for row in matrix if row['entry_type']=='http'} if matrix_http != indexed_routes: raise SystemExit('HTTP Route 与可达操作索引双向覆盖不一致') linked={name for row in matrix for name in row['requirements']} if linked != spec_requirements: raise SystemExit('入口矩阵未双向覆盖全部行为 Requirement') worker_source=Path('pkg/queue/handler.go').read_text()+Path('cmd/worker/main.go').read_text() registered={'constants.'+name for name in re.findall(r'(?:HandleFunc|Register)\(constants\.(TaskType\w+|OutboxEventType\w+)', worker_source)} matrix_async={row['entry'] for row in matrix if row['entry_type']=='async'} if matrix_async != registered: raise SystemExit('异步注册入口与证据矩阵双向覆盖不一致') PY tmpdir=$(mktemp -d) generated=docs/admin-openapi.yaml had_generated=0 if [[ -f "$generated" ]]; then had_generated=1 cp "$generated" "$tmpdir/original.yaml" fi cleanup_generated() { if (( had_generated )); then cp "$tmpdir/original.yaml" "$generated"; else rm -f "$generated"; fi rm -rf "$tmpdir" } trap cleanup_generated EXIT go run cmd/gendocs/main.go >/dev/null 2>&1 cp "$generated" "$tmpdir/first.yaml" go run cmd/gendocs/main.go >/dev/null 2>&1 cmp -s "$tmpdir/first.yaml" "$generated" || { print -u2 'OpenAPI 连续生成结果不一致'; exit 1; } print 'Context 健康检查通过'