agent_profile.py
5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""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/<org_node>/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: `<target_folder_path>/.claude/agents/<org_node>.md`. Confirmed
with user 2026-07-31: 03-C §3.3 ("让整个会话以目标项目 .claude/agents/<org_node>.md
定义的 subagent 身份运行") and 05 §5.1 are the authoritative SDD contract here --
Claude Code's `--agent <name>` 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",
],
"tools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
},
"validator": {
"memory": "project",
"model": "claude-sonnet-5",
"skills": ["qa-task-validation", "unattended-agent-operations"],
"tools": ["Read", "Bash", "Grep", "Glob"],
"disallowedTools": [
"Edit",
"Write",
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(git reset:*)",
"Bash(git clean:*)",
],
},
}
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/<org_node>.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()