"""Org Agent identity: compiles migrated Hermes personas into Claude Code subagent definitions and deploys them into a target project (U-004, A-2 assumption). Source content: `agents//SOUL.md` (+ `AGENTS.md` for validator) in graphlemon's own repo -- the migrated/adapted personas, see those files' own migration notes for what changed and why. Deploy target: `/.claude/agents/.md`. Confirmed with user 2026-07-31: 03-C §3.3 ("让整个会话以目标项目 .claude/agents/.md 定义的 subagent 身份运行") and 05 §5.1 are the authoritative SDD contract here -- Claude Code's `--agent ` discovers subagents relative to the invocation cwd, so the file has to physically be inside whatever directory `claude` is run from (the target project), not graphlemon's own repo. Architecture doc §13.1's "agents/ is a platform-only asset" framing is about the *canonical* copy (this module's source), not the deployed one -- the two aren't the same file. agent_profile (01-M 1.9) is a singleton per org_node, has no target-project reference -- it tracks graphlemon's own canonical definition, not a per-deploy record, so deploy_agent_definitions() doesn't touch it; record_agent_profiles() does, separately. """ import os _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _AGENTS_SOURCE_DIR = os.path.join(_REPO_ROOT, "agents") ORG_NODES = ("planner", "coder", "validator") _MIGRATED_FROM_PROFILE = { "planner": "planning", "coder": "code", "validator": "qa", } _DESCRIPTIONS = { "planner": "graphlemon Planner: turns a goal into BRD/PRD/SDD/Benchmark artifacts. Never writes business code.", "coder": "graphlemon Coder: implements one task's code + tests against the Planner's SDD.", "validator": "graphlemon Validator: independently verifies a task, PASS/FAIL/INCONCLUSIVE, read-only on source.", } # 05 §5.1: skills:/memory:/tools:/model: (and, for validator, disallowedTools: -- # the qa/AGENTS.md workspace-permission-contract folded in) are the frontmatter # fields U-004's Done criterion checks for. FRONTMATTER = { "planner": { "memory": "project", "model": "claude-sonnet-5", "skills": [ "planning-requirement-expansion", "project-brd-planning", "project-prd-planning", "project-sdd-planning", "project-unit-decomposition", "review-blocker-verification", ], "tools": ["Read", "Write", "Grep", "Glob"], }, "coder": { "memory": "project", "model": "claude-sonnet-5", "skills": [ "code-task-development", "code-agent-engineering", "debugging-toolkit", "software-planning-execution", "design-dynamic-work-graph", ], "tools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"], }, "validator": { "memory": "project", "model": "claude-sonnet-5", "skills": [ "qa-task-validation", "unattended-agent-operations", "design-dynamic-work-graph", "unknown-scope-defect-sweep", ], "tools": ["Read", "Bash", "Grep", "Glob"], # 2026-07-31 fix, found for real running U-014's full closed loop: # scoped Bash patterns here ("Bash(git commit:*)") don't just block # that one subcommand -- they silently disable Bash *entirely* for # the agent (confirmed by isolating it: a plain ["Edit","Write"] # disallowedTools list leaves Bash working fine; adding any # "Bash(...)" entry breaks it, even though the CLI's own # --disallowedTools flag documents this exact scoped syntax as # supported -- it just doesn't work the same way in subagent # frontmatter). A Validator that can't run tests at all is a much # worse outcome than the residual risk of it running `git commit`, # so the git-mutation restriction now relies on the prose instruction # already in the migrated AGENTS.md ("不提交、推送、reset、清理或覆盖用户 # 工作树") instead of a tool-level block that doesn't actually work. # ponytail: this is a real gap (no enforced ceiling on Bash git # mutations, just an asked-nicely one) -- upgrade path is whatever # Claude Code's correct scoped-disallowedTools frontmatter syntax # turns out to be, if this gets fixed/documented upstream. "disallowedTools": ["Edit", "Write"], }, } def _soul_path(org_node): return os.path.join(_AGENTS_SOURCE_DIR, org_node, "SOUL.md") def _agents_md_path(org_node): return os.path.join(_AGENTS_SOURCE_DIR, org_node, "AGENTS.md") def _read(path): with open(path, encoding="utf-8") as f: return f.read() def _frontmatter_yaml(org_node, fm): lines = ["---", f"name: {org_node}", f"description: {_DESCRIPTIONS[org_node]}"] lines.append(f"memory: {fm['memory']}") lines.append(f"model: {fm['model']}") lines.append("skills:") lines += [f" - {s}" for s in fm["skills"]] lines.append("tools:") lines += [f" - {t}" for t in fm["tools"]] if "disallowedTools" in fm: lines.append("disallowedTools:") lines += [f" - {t}" for t in fm["disallowedTools"]] lines.append("---") return "\n".join(lines) def render_agent_definition(org_node): """Assemble the .claude/agents/.md content: frontmatter + migrated body.""" if org_node not in ORG_NODES: raise ValueError(org_node) body = _read(_soul_path(org_node)) agents_md_path = _agents_md_path(org_node) if os.path.exists(agents_md_path): body += "\n\n" + _read(agents_md_path) return _frontmatter_yaml(org_node, FRONTMATTER[org_node]) + "\n\n" + body def deploy_agent_definitions(target_folder_path): """Write .claude/agents/{planner,coder,validator}.md into a target project.""" out_dir = os.path.join(target_folder_path, ".claude", "agents") os.makedirs(out_dir, exist_ok=True) for org_node in ORG_NODES: with open(os.path.join(out_dir, org_node + ".md"), "w", encoding="utf-8") as f: f.write(render_agent_definition(org_node)) def record_agent_profiles(conn, current_git_commit_sha): """Upsert the agent_profile (01-M 1.9) singleton rows for graphlemon's own repo.""" for org_node in ORG_NODES: agents_md_path = _agents_md_path(org_node) agents_file_path = os.path.relpath(agents_md_path, _REPO_ROOT) if os.path.exists(agents_md_path) else None conn.execute( "INSERT INTO agent_profile " "(org_node, soul_file_path, agents_file_path, migrated_from_profile, current_git_commit_sha) " "VALUES (?, ?, ?, ?, ?) " "ON CONFLICT(org_node) DO UPDATE SET " "soul_file_path=excluded.soul_file_path, " "agents_file_path=excluded.agents_file_path, " "migrated_from_profile=excluded.migrated_from_profile, " "current_git_commit_sha=excluded.current_git_commit_sha", ( org_node, os.path.relpath(_soul_path(org_node), _REPO_ROOT), agents_file_path, _MIGRATED_FROM_PROFILE[org_node], current_git_commit_sha, ), ) conn.commit()