Skip to content

Commit cc00d06

Browse files
committed
feat: conditionally inject Semaphore block into SOUL.md when enabled
1 parent 5e2c54f commit cc00d06

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

internal/resources/semaphore.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,48 @@ func BuildSemaphoreSkill(inst *hermesv1.HermesInstance) *hermesv1.InstanceSkill
5353
func SemaphoreCleanupOnDelete(inst *hermesv1.HermesInstance) bool {
5454
return SemaphoreEnabled(inst) && BoolValue(inst.Spec.Semaphore.CleanupOnDelete)
5555
}
56+
57+
// SemaphoreSoulBlock returns the markdown block injected into SOUL.md
58+
// when Semaphore integration is enabled. It includes the API quickstart
59+
// and skill loading instructions.
60+
const SemaphoreSoulBlock = `
61+
62+
# Semaphore UI — infrastructure automation
63+
64+
You have access to a **Semaphore UI** instance for running Ansible
65+
playbooks, Terraform/OpenTofu plans, and general scripts with full
66+
run logs and audit trails.
67+
68+
- **URL**: ` + "`$SEMAPHORE_URL`" + ` (injected automatically)
69+
- **Auth**: ` + "`$SEMAPHORE_TOKEN`" + ` (per-agent, scoped to this project)
70+
- **Skill**: Load the ` + "`semaphore-ui`" + ` skill (` + "`skill_view(name='semaphore-ui')`" + `)
71+
for the full API reference, then use Semaphore for any multi-step
72+
infrastructure operation that needs logging, rollback, or approval gates.
73+
74+
When given an infra task that spans multiple hosts or needs an audit trail
75+
(Terraform, Ansible, DB migration, cluster upgrade), **default to Semaphore**.
76+
The ` + "`semaphore-ui`" + ` skill has templates for common tasks.
77+
78+
**Quickstart** (fallback if skill not loaded):
79+
80+
` + "```" + `python
81+
import http.client, json, os
82+
83+
url = os.environ["SEMAPHORE_URL"]
84+
token = os.environ["SEMAPHORE_TOKEN"]
85+
host = url.split("://")[1].split(":")[0]
86+
port = int(url.split(":")[-1])
87+
88+
def sema(method, path, body=None):
89+
c = http.client.HTTPConnection(host, port)
90+
h = {"Authorization": f"Bearer {token}"}
91+
if body:
92+
h["Content-Type"] = "application/json"
93+
body = json.dumps(body)
94+
c.request(method, path, body, h)
95+
return json.loads(c.getresponse().read())
96+
97+
# your projects (token-scoped)
98+
print(sema("GET", "/api/projects"))
99+
` + "```" + `
100+
`

internal/resources/workspace_configmap.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ func DecodeWorkspacePath(key string) string {
3838
func BuildWorkspaceConfigMap(inst *hermesv1.HermesInstance) *corev1.ConfigMap {
3939
data := map[string]string{}
4040
for _, f := range inst.Spec.Workspace.InitialFiles {
41-
data[EncodeWorkspacePath(f.Path)] = f.Content
41+
content := f.Content
42+
if f.Path == "SOUL.md" && SemaphoreEnabled(inst) {
43+
content += SemaphoreSoulBlock
44+
}
45+
data[EncodeWorkspacePath(f.Path)] = content
4246
}
4347
if len(inst.Spec.Workspace.InitialDirs) > 0 {
4448
dirs := make([]string, len(inst.Spec.Workspace.InitialDirs))

0 commit comments

Comments
 (0)