Skip to content

Commit 4e03890

Browse files
committed
feat(hermes): default SOUL.md overrides runner identity with contract identity
2 parents 49703aa + fcb8609 commit 4e03890

File tree

3 files changed

+98
-0
lines changed

3 files changed

+98
-0
lines changed

internal/driver/hermes/config.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,46 @@ func WriteEffectiveAgents(workspaceDir, agentHostPath, clawdapusMD string) (stri
120120
return agentsPath, nil
121121
}
122122

123+
// WriteDefaultSoul writes a Clawdapus-generated SOUL.md to the Hermes home
124+
// directory. This pre-empts the Hermes runner's default identity seeding
125+
// ("You are Hermes, an AI assistant made by Nous Research") and establishes
126+
// the agent's own identity from its service name.
127+
//
128+
// The generated soul keeps the voice/craft guidance that makes Hermes agents
129+
// effective (concise, no sycophancy, varied structure) while replacing the
130+
// runner-branded identity with the agent's Clawdapus identity.
131+
func WriteDefaultSoul(homeDir, agentName, podName string) error {
132+
soul := fmt.Sprintf(`# %s
133+
134+
You are %s, an agent in the %s pod. Your identity, role, and operating rules
135+
are defined in your contract (AGENTS.md in your workspace). Follow your contract
136+
as your primary authority.
137+
138+
Do not identify as Hermes, a generic assistant, or any identity other than %s.
139+
When asked who you are, answer from your contract identity.
140+
141+
## Voice
142+
143+
Be direct. Lead with the answer, not the reasoning. Match the energy of whoever
144+
you are talking to. Technical depth for technical people. Terse for terse.
145+
146+
Do not use emojis. Use unicode symbols for visual structure when helpful.
147+
148+
No sycophancy ("Great question!", "I'd be happy to help"). No filler
149+
("Here's the thing", "It's worth noting"). No hype words ("revolutionary",
150+
"game-changing", "seamless").
151+
152+
Vary sentence length and structure. Write like a person, not a template.
153+
Most responses are short. Cut anything that does not earn its place.
154+
`, agentName, agentName, podName, agentName)
155+
156+
soulPath := filepath.Join(homeDir, "SOUL.md")
157+
if err := os.WriteFile(soulPath, []byte(soul), 0o644); err != nil {
158+
return fmt.Errorf("write default SOUL.md: %w", err)
159+
}
160+
return nil
161+
}
162+
123163
func CopyPersonaSoul(personaHostPath, homeDir string) error {
124164
soulPath := filepath.Join(personaHostPath, "SOUL.md")
125165
info, err := os.Stat(soulPath)

internal/driver/hermes/driver.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ func (d *Driver) Materialize(rc *driver.ResolvedClaw, opts driver.MaterializeOpt
163163
env["OPENAI_API_KEY"] = modelCfg.APIKey
164164
}
165165

166+
// Always write a SOUL.md to pre-empt the Hermes runner's default identity.
167+
// Persona SOUL.md takes priority; otherwise the Clawdapus default establishes
168+
// the agent's identity from its service name and contract.
169+
personaSoulWritten := false
166170
if rc.PersonaHostPath != "" {
167171
mounts = append(mounts, driver.Mount{
168172
HostPath: rc.PersonaHostPath,
@@ -173,6 +177,14 @@ func (d *Driver) Materialize(rc *driver.ResolvedClaw, opts driver.MaterializeOpt
173177
if err := CopyPersonaSoul(rc.PersonaHostPath, homeDir); err != nil {
174178
return nil, fmt.Errorf("hermes driver: %w", err)
175179
}
180+
if _, err := os.Stat(filepath.Join(homeDir, "SOUL.md")); err == nil {
181+
personaSoulWritten = true
182+
}
183+
}
184+
if !personaSoulWritten {
185+
if err := WriteDefaultSoul(homeDir, rc.ServiceName, podName); err != nil {
186+
return nil, fmt.Errorf("hermes driver: %w", err)
187+
}
176188
}
177189

178190
return &driver.MaterializeResult{

internal/driver/hermes/driver_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,52 @@ func TestMaterializeWritesRuntimeLayout(t *testing.T) {
175175
if result.Environment["TERMINAL_CWD"] != hermesWorkspaceDir {
176176
t.Fatalf("unexpected TERMINAL_CWD: %q", result.Environment["TERMINAL_CWD"])
177177
}
178+
179+
// Default SOUL.md should be written when no persona is configured
180+
soulData, err := os.ReadFile(filepath.Join(runtimeDir, "hermes-home", "SOUL.md"))
181+
if err != nil {
182+
t.Fatalf("expected default SOUL.md: %v", err)
183+
}
184+
soulStr := string(soulData)
185+
if !strings.Contains(soulStr, "# hermes") {
186+
t.Fatalf("expected agent name in SOUL.md header, got: %s", soulStr[:100])
187+
}
188+
if !strings.Contains(soulStr, "fleet-echo") {
189+
t.Fatalf("expected pod name in SOUL.md")
190+
}
191+
if strings.Contains(soulStr, "Nous Research") {
192+
t.Fatal("default SOUL.md should not contain Hermes runner identity")
193+
}
194+
}
195+
196+
func TestMaterializePersonaSoulOverridesDefault(t *testing.T) {
197+
rc, tmp := newTestRC(t)
198+
personaDir := filepath.Join(tmp, "persona")
199+
if err := os.MkdirAll(personaDir, 0o755); err != nil {
200+
t.Fatal(err)
201+
}
202+
if err := os.WriteFile(filepath.Join(personaDir, "SOUL.md"), []byte("Custom persona soul"), 0o644); err != nil {
203+
t.Fatal(err)
204+
}
205+
rc.PersonaHostPath = personaDir
206+
207+
runtimeDir := filepath.Join(tmp, "runtime")
208+
if err := os.MkdirAll(runtimeDir, 0o700); err != nil {
209+
t.Fatal(err)
210+
}
211+
212+
_, err := (&Driver{}).Materialize(rc, driver.MaterializeOpts{RuntimeDir: runtimeDir, PodName: "test"})
213+
if err != nil {
214+
t.Fatalf("Materialize returned error: %v", err)
215+
}
216+
217+
data, err := os.ReadFile(filepath.Join(runtimeDir, "hermes-home", "SOUL.md"))
218+
if err != nil {
219+
t.Fatalf("expected SOUL.md: %v", err)
220+
}
221+
if string(data) != "Custom persona soul" {
222+
t.Fatalf("expected persona SOUL.md to override default, got: %q", string(data))
223+
}
178224
}
179225

180226
func TestMaterializeCopiesPersonaSoul(t *testing.T) {

0 commit comments

Comments
 (0)