agent_profile.py
7.19 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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""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",
"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/<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()