Skip to content
Merged
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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Syntax check (node + bash)
run: npm run lint:syntax
- name: Unit tests
run: npm test
- name: Validate all 57 regimes
run: npm run validate:regimes
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# 📜 Changelog

## v5.0.0 (2026-04-14) — Learning Loop 🧠

Inspired by [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent). CivAgent gains a **cross-match learning loop**: civilizations now accumulate governance skills as they play.

### New
- **Civilization memory isolation** — each regime runs in its own `~/.civagent/envs/<region>-<id>/` with isolated `HOME` + `XDG_*` paths. No cross-contamination between civs.
- **Automatic skill sedimentation** — after each match, `codex` extracts reusable governance patterns from the transcript, `gemini` audits them for shape/quality, and approved skills are written to `regimes/<civ>/skills/learned-<date>-<topic>-<matchId>.md` for use in future matches.
- **Prompt-injection guard** on learned skills — rejects patterns containing role-redirection tokens, jailbreak strings, or missing frontmatter. Each skill file carries a provenance banner.
- **New CLI**: `civagent run --v5`, `civagent skills <regime>`, `civagent match-log`, `civagent tournament`.
- **Tournament mode** — spawn 4 civilizations against the same task in parallel, auto-judge governance quality.
- **package.json + unit tests + CI** — `npm test`, GitHub Actions pipeline on PR.

### Design notes
- v5 is fully opt-in via `--v5` flag; v4 behavior preserved.
- Three independent AI review passes (Codex → Gemini → Kimi) shaped the final design. See [docs/V5-DESIGN.md](./docs/V5-DESIGN.md).

### Limitations documented
- Compressing a governance system to one agent's `SOUL.md` is lossy; multi-department sub-agent splits are a v5.2 candidate.
- `regimes/` has no time-dimension; anachronistic comparisons are a feature, not a bug.

---

## v3.5.2 (2026-03-13)

### Bug 修复
Expand Down
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,41 @@
<img src="./images/civagent-v4-banner.svg" alt="CivAgent v4 Banner" width="100%" />
</p>

# 🎮 CivAgent v4 — 选择你的文明,指挥你的 AI 团队
# 🎮 CivAgent v5 — 选择你的文明,指挥你的 AI 团队,让文明自我学习

### 人类 5000 年治国智慧 × AI 多 Agent 协作 | 57 种政体 · 6 种编排模式 · 10 个模型 · Claude Code 驱动
### 人类 5000 年治国智慧 × AI 多 Agent 协作 × 跨局技能沉淀 | 57 种政体 · 6 种编排模式 · 10 个模型 · Claude Code 驱动

<p align="center">
<img src="https://img.shields.io/badge/文明-57种-gold?style=for-the-badge" />
<img src="https://img.shields.io/badge/中华朝代-20个-red?style=for-the-badge" />
<img src="https://img.shields.io/badge/世界帝国-37个-blue?style=for-the-badge" />
<img src="https://img.shields.io/badge/版本-v5.0.0-gold?style=for-the-badge" />
<img src="https://img.shields.io/badge/文明-57种-red?style=for-the-badge" />
<img src="https://img.shields.io/badge/编排模式-6种-purple?style=for-the-badge" />
<img src="https://img.shields.io/badge/Runtime-Claude_Code-blueviolet?style=for-the-badge" />
<img src="https://img.shields.io/badge/模型-10个-orange?style=for-the-badge" />
<img src="https://img.shields.io/badge/学习闭环-Hermes_inspired-blueviolet?style=for-the-badge" />
</p>

<div align="center">

### ⚡ v5 新能力:学习闭环

每局对局结束后,Codex 从对话记录中提取治理经验,Gemini 审查通过后写入 `regimes/<civ>/skills/`。下一局同文明自动加载历史智慧——**文明会随着对局越打越聪明**。

```bash
civagent switch china/tang
civagent run --v5 "如何应对边境饥荒?" # v5 模式:隔离记忆 + 自动沉淀
civagent skills china/tang # 查看唐朝累积的治理经验
civagent tournament \ # 4 文明对战同一题目
--civs china/tang,china/qin,global/athens,global/roman-republic \
"how do we handle a famine on the eastern frontier?"
```

灵感来源:[NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)

</div>

---

<div align="center">

> *「每一代的制度都是对上一代制度弊端的回应。」*
> *— 钱穆《中国历代政治得失》*

Expand Down
43 changes: 33 additions & 10 deletions bin/civagent
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,27 @@ CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'

usage() {
cat <<EOF
${BOLD}CivAgent v4${NC} — 5000 Years of Governance Wisdom × AI Multi-Agent Orchestration
${BOLD}CivAgent v5${NC} — 5000 Years of Governance × AI Multi-Agent × Learning Loop

${BOLD}Usage:${NC}
civagent list List all 57 regimes
civagent info <regime> Show regime details
civagent switch <regime> Set active regime
civagent run [prompt] Launch CC with active regime's agents
civagent agents Show generated CC agents for active regime
civagent modes List 6 orchestration modes
civagent setup Check tool availability (CC, Codex, Gemini, cn-cc)
civagent list List all 57 regimes
civagent info <regime> Show regime details
civagent switch <regime> Set active regime
civagent run [prompt] Launch CC with active regime's agents (v4 mode)
civagent run --v5 "task" Launch v5 with isolated HOME + skill sedimentation
civagent skills <regime> List learned skills for a regime
civagent match-log Recent match transcripts
civagent tournament --civs a,b,c,d "task"
Parallel match across civilizations + judge ranking
civagent agents Show generated CC agents for active regime
civagent modes List 6 orchestration modes
civagent setup Check tool availability (CC, Codex, Gemini, cn-cc)

${BOLD}Examples:${NC}
civagent switch china/tang
civagent run "重构这个模块的代码"
civagent run --mode democratic "设计新的 API 接口"
civagent run --v5 "重构这个模块的代码"
civagent tournament --civs china/tang,china/qin,global/athens,global/roman-republic \\
"how do we handle a famine on the eastern frontier?"

${BOLD}Regimes:${NC}
20 Chinese dynasties: xia, shang, zhou, qin, han, tang, song, ming, qing, ...
Expand Down Expand Up @@ -276,6 +282,22 @@ cmd_match_log() {
done
}

cmd_tournament() {
local civs=""
local rest=()
while [[ $# -gt 0 ]]; do
case "$1" in
--civs) civs="$2"; shift 2 ;;
*) rest+=("$1"); shift ;;
esac
done
if [[ -z "$civs" || ${#rest[@]} -eq 0 ]]; then
echo "Usage: civagent tournament --civs a/x,b/y,c/z \"task prompt\""
exit 1
fi
exec node "$ENGINE_DIR/v5/tournament.mjs" --civs "$civs" "${rest[@]}"
}

# ── main ─────────────────────────────────────────────────────────────────────

case "${1:-}" in
Expand All @@ -288,6 +310,7 @@ case "${1:-}" in
setup) cmd_setup ;;
skills) cmd_skills "${2:-}" ;;
match-log) cmd_match_log ;;
tournament) shift; cmd_tournament "$@" ;;
help|--help|-h|"") usage ;;
*) echo "Unknown command: $1"; usage; exit 1 ;;
esac
14 changes: 13 additions & 1 deletion engine/regime-to-cc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
import fs from "node:fs";
import path from "node:path";

// Historical metadata used non-canonical pattern names. Normalize to the 6
// mode files under engine/modes/ so template references resolve correctly.
const PATTERN_ALIASES = {
"centralized-hierarchy": "centralized",
"democratic-council": "democratic",
"federated-autonomy": "federation",
"dual-power": "dual-track",
};
function normalizePattern(p) {
return PATTERN_ALIASES[p] || p || "centralized";
}

const ROLE_MODEL_MAP = {
coordinator: { model: "sonnet", role: "coordinator" },
engineering: { model: "opus", role: "engineering" },
Expand Down Expand Up @@ -166,7 +178,7 @@ function buildClaudeMd(metadata, soul, identity) {
const regimeEn = metadata.name?.en || metadata.id;
const era = metadata.era?.zh || "";
const system = metadata.system?.zh || "";
const pattern = metadata.orchestrationPattern || "centralized";
const pattern = normalizePattern(metadata.orchestrationPattern);

return `# CivAgent v4 — ${regime} (${regimeEn})

Expand Down
102 changes: 102 additions & 0 deletions engine/v5/tournament.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env node
// tournament.mjs — run a single governance task against N civilizations in parallel,
// collect transcripts, have a judge model rank outcomes.

import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { spawn, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { validateRegime } from "./civ-memory.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
const TOURNAMENTS_DIR = path.join(os.homedir(), ".civagent", "tournaments");

const JUDGE_PROMPT = `You are the judge of a CivAgent governance tournament.
Each civilization received the same task and produced a transcript of how its
governance system responded. Rank them on:
- legality (did they respect their own rules?)
- feasibility (are the actions executable?)
- resilience (would this survive second-order effects?)

Output ONLY a markdown table with columns: Rank | Civilization | Score /10 | One-line reason.
Then one paragraph: "## Verdict" explaining the top choice.`;

function newTournamentId() {
return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
}

function runCiv(regime, task, outDir) {
return new Promise(resolve => {
const logFile = path.join(outDir, `${regime.replace(/\//g, "-")}.log`);
const out = fs.createWriteStream(logFile);
const binary = path.join(PROJECT_ROOT, "bin", "civagent");
const proc = spawn(binary, ["switch", regime], { stdio: ["ignore", "pipe", "pipe"] });
proc.stdout.pipe(out, { end: false });
proc.stderr.pipe(out, { end: false });
proc.on("close", () => {
const run = spawn(binary, ["run", "--v5", task], {
stdio: ["ignore", "pipe", "pipe"],
});
run.stdout.pipe(out, { end: false });
run.stderr.pipe(out, { end: false });
run.on("close", code => {
out.end();
resolve({ regime, code, logFile });
});
});
});
}

async function judge(task, civResults, outDir) {
const sections = civResults.map(r => {
const content = fs.existsSync(r.logFile)
? fs.readFileSync(r.logFile, "utf8").slice(-6000)
: "(no output)";
return `### ${r.regime} (exit ${r.code})\n\n\`\`\`\n${content}\n\`\`\``;
}).join("\n\n---\n\n");

const prompt = `${JUDGE_PROMPT}\n\n## Task\n${task}\n\n## Civilization Transcripts\n\n${sections}`;
const r = spawnSync("gemini", ["-p", prompt], {
encoding: "utf8", timeout: 300_000, env: process.env,
});
if (r.status !== 0) {
return `# Tournament Result — judge unavailable\n\nGemini failed: ${r.stderr || r.error?.message}\n\nRaw civ exit codes:\n${civResults.map(c => `- ${c.regime}: ${c.code}`).join("\n")}`;
}
return `# Tournament — ${new Date().toISOString()}\n\n**Task:** ${task}\n\n${r.stdout}`;
}

export async function runTournament({ civs, task }) {
if (!civs.length || !task) throw new Error("need --civs and a task");
civs.forEach(validateRegime);

const id = newTournamentId();
const outDir = path.join(TOURNAMENTS_DIR, id);
fs.mkdirSync(outDir, { recursive: true });

console.error(`[tournament] ${id} civs=${civs.join(",")} out=${outDir}`);
const results = await Promise.all(civs.map(c => runCiv(c, task, outDir)));

const verdictMd = await judge(task, results, outDir);
const resultFile = path.join(outDir, "result.md");
fs.writeFileSync(resultFile, verdictMd);
console.log(`\n==== Tournament ${id} ====`);
console.log(verdictMd);
return { id, resultFile, results };
}

if (import.meta.url === `file://${process.argv[1]}`) {
const args = process.argv.slice(2);
let civs = [];
const rest = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === "--civs" && args[i + 1]) {
civs = args[++i].split(",").map(s => s.trim()).filter(Boolean);
} else {
rest.push(args[i]);
}
}
const task = rest.join(" ").trim();
runTournament({ civs, task }).catch(e => { console.error(e); process.exit(1); });
}
30 changes: 30 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "civagent",
"version": "5.0.0",
"description": "CivAgent — 57 historical governance systems × AI multi-agent orchestration on Claude Code, with cross-match skill sedimentation",
"bin": {
"civagent": "bin/civagent"
},
"scripts": {
"test": "node --test test/*.test.mjs",
"lint:syntax": "node -c engine/v5/civ-memory.mjs && node -c engine/v5/skill-sediment.mjs && node -c engine/v5/run-v5.mjs && node -c engine/regime-to-cc.mjs && bash -n bin/civagent",
"validate:regimes": "node test/regime-validator.mjs"
},
"type": "module",
"engines": {
"node": ">=18"
},
"repository": {
"type": "git",
"url": "https://github.com/LeoLin990405/civagent"
},
"license": "MIT",
"keywords": [
"claude-code",
"multi-agent",
"civilization",
"governance",
"hermes-agent",
"skill-sedimentation"
]
}
36 changes: 36 additions & 0 deletions regimes/AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Regime Audit — v5.0.0

Generated by `npm run validate:regimes` on 2026-04-14.

## Summary

- **57 regimes** total: 20 Chinese dynasties + 37 global empires
- **100% pass** mechanical validation (metadata.json valid, required fields present, IDENTITY.md + SOUL.md exist)
- **40 regimes** use non-canonical `orchestrationPattern` names (aliases) — normalized at runtime, documented below

## Canonical vs Alias patterns

The engine supports 6 canonical orchestration modes. Some regimes inherited alias names from upstream; `engine/regime-to-cc.mjs` now normalizes them:

| Canonical | Alias | Regime count using alias |
|---|---|---|
| `centralized` | `centralized-hierarchy` | ~25 |
| `democratic` | `democratic-council` | ~8 |
| `federation` | `federated-autonomy` | ~4 |
| `dual-track` | `dual-power` | ~3 |
| `checks-and-balances` | *(no alias)* | — |
| `theocratic` | *(no alias)* | — |

Aliases are accepted for backward compatibility. **Future contributors should use the canonical name** when adding new regimes.

## Known design limitations

1. **Regime-to-agent compression**: a governance system collapses into a single agent's `SOUL.md`. Multi-department splits (each ministry as a sub-agent) are a v5.2 candidate. See [docs/V5-DESIGN.md](../docs/V5-DESIGN.md).
2. **No time dimension**: `regimes/` mixes ancient dynasties with modern states (`china/tang` next to `usa/federal`). Cross-era comparisons are a feature, not a bug.
3. **Attribution to upstream**: 57 regimes were inherited from `wanikua/danghuangshang`. See [CREDITS.md](../CREDITS.md).

## Regenerate this report

```bash
npm run validate:regimes
```
Loading
Loading