@delorenj/pjangler 1.3.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mise/scripts/link-agentfiles.sh +38 -5
- package/README.md +92 -0
- package/dist/assets/project-notebook-skill/SHA256SUMS +10 -0
- package/dist/assets/project-notebook-skill/SKILL.md +64 -0
- package/dist/assets/project-notebook-skill/agents/openai.yaml +6 -0
- package/dist/assets/project-notebook-skill/export-manifest.json +56 -0
- package/dist/assets/project-notebook-skill/hooks/claude.settings.json +26 -0
- package/dist/assets/project-notebook-skill/hooks/hooks.master.json +26 -0
- package/dist/assets/project-notebook-skill/hooks/session-end.sh +228 -0
- package/dist/assets/project-notebook-skill/hooks/session-start.sh +228 -0
- package/dist/assets/project-notebook-skill/references/configuration.md +93 -0
- package/dist/assets/project-notebook-skill/references/recovery.md +54 -0
- package/dist/assets/project-notebook-skill/scripts/project-hooks.py +865 -0
- package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +848 -0
- package/dist/index.js +11307 -2809
- package/dist/mcp-server.js +9637 -2094
- package/dist/prompt.js +404 -0
- package/package.json +8 -5
- package/templates/commonproject/copier.yml +19 -5
- package/templates/commonproject/template/.mise/scripts/link-agentfiles.sh +38 -5
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +74 -52
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +479 -24
- package/templates/commonproject/template/mise.toml.jinja +12 -6
- package/templates/hermes-agent/copier.yml +8 -11
- package/templates/hermes-agent/template/.gitignore.jinja +1 -0
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
- package/templates/hermes-agent/template/.scripts/01-config.sh +9 -0
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +68 -4
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +77 -43
- package/templates/hermes-agent/template/.scripts/80-registry.sh +6 -0
- package/templates/hermes-agent/template/.scripts/_lib.sh +116 -16
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +13 -1
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +761 -0
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +19 -3
- package/templates/hermes-agent/template/SOUL.md.jinja +44 -8
- package/templates/hermes-agent/template/hermes.jinja +20 -8
- package/templates/hermes-agent/template/momo.jinja +177 -0
- package/templates/hermes-agent/template/role.yaml.jinja +19 -19
|
@@ -12,20 +12,26 @@ _.file = [".env"]
|
|
|
12
12
|
# i.e. All linked agent files MUST be siblings at
|
|
13
13
|
# any given level of nesting.
|
|
14
14
|
#
|
|
15
|
+
# NOTE: every managed script is handed config_root EXPLICITLY. A mise enter
|
|
16
|
+
# hook runs with cwd set to the directory you cd'd into -- including a PARENT
|
|
17
|
+
# config's hook -- so a script that reads its subject from cwd reshapes
|
|
18
|
+
# whichever nested repo you entered. config_root locates the file; the
|
|
19
|
+
# argument locates the subject. The scripts refuse to run without it.
|
|
20
|
+
#
|
|
15
21
|
# NOTE: hooks are an array of [[hooks.enter]] tables (NOT enter = [ ... ]) and
|
|
16
22
|
# every {{config_root}} path is single-quoted so it survives a space in the
|
|
17
23
|
# project path — mise runs each `script` via `sh -c`.
|
|
18
24
|
[[hooks.enter]]
|
|
19
|
-
script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
|
|
25
|
+
script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh' '{% raw %}{{config_root}}{% endraw %}'"
|
|
20
26
|
[[hooks.enter]]
|
|
21
27
|
# Atomic (PJAN-24/PJAN-57): shell complexity lives in the managed script so
|
|
22
28
|
# mise never interpolates temporary variables. The script uses mktemp, traps
|
|
23
29
|
# cleanup, and replaces .env only after op inject succeeds.
|
|
24
30
|
script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/materialize-env.sh'"
|
|
25
31
|
[[hooks.enter]]
|
|
26
|
-
script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py'"
|
|
32
|
+
script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py' --root '{% raw %}{{config_root}}{% endraw %}'"
|
|
27
33
|
[[hooks.enter]]
|
|
28
|
-
script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project"
|
|
34
|
+
script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project --root '{% raw %}{{config_root}}{% endraw %}'"
|
|
29
35
|
[[hooks.enter]]
|
|
30
36
|
script = "[ -f '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' ] && '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' || true"
|
|
31
37
|
{%- if agent_hooks_layer %}
|
|
@@ -51,16 +57,16 @@ task = "hooks:sync"
|
|
|
51
57
|
{% endif %}
|
|
52
58
|
[tasks."link:agentfiles"]
|
|
53
59
|
description = "Symlink all agent files to AGENTS.md"
|
|
54
|
-
run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
|
|
60
|
+
run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh' '{% raw %}{{config_root}}{% endraw %}'"
|
|
55
61
|
|
|
56
62
|
[tasks."skills:sync"]
|
|
57
63
|
description = "Sync skills from manifest to local CLI dirs"
|
|
58
64
|
depends = ["skills:provision:packs"]
|
|
59
|
-
run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project"
|
|
65
|
+
run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project --root '{% raw %}{{config_root}}{% endraw %}'"
|
|
60
66
|
|
|
61
67
|
[tasks."skills:provision:packs"]
|
|
62
68
|
description = "Provision every Skillex pack declared in .agents/skills.json"
|
|
63
|
-
run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py'"
|
|
69
|
+
run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py' --root '{% raw %}{{config_root}}{% endraw %}'"
|
|
64
70
|
{% if agent_hooks_layer %}
|
|
65
71
|
# --- Project-scoped agent hooks + skill fan-out (see .agents/hooks/README.md) ---
|
|
66
72
|
|
|
@@ -46,17 +46,14 @@ target_repo:
|
|
|
46
46
|
|
|
47
47
|
role:
|
|
48
48
|
type: str
|
|
49
|
-
help: "Role for this agent"
|
|
49
|
+
help: "Role for this agent (safe single path segment; arbitrary roles are supported)"
|
|
50
50
|
default: pm
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"Ops (ops)": ops
|
|
58
|
-
"QA (qa)": qa
|
|
59
|
-
validator: "{% if not role %}Required{% endif %}"
|
|
51
|
+
validator: >-
|
|
52
|
+
{% set alnum = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' -%}
|
|
53
|
+
{% set safe_chars = alnum ~ '._-' -%}
|
|
54
|
+
{% if not role or role != role|trim or role in ['.', '..'] or role[0] not in alnum or role[-1] not in alnum or role|reject('in', safe_chars)|list|length -%}
|
|
55
|
+
Role must be a non-empty safe single path segment using letters, numbers, dots, underscores, or hyphens
|
|
56
|
+
{%- endif %}
|
|
60
57
|
|
|
61
58
|
ticket_provider:
|
|
62
59
|
type: str
|
|
@@ -157,7 +154,7 @@ plane_workspace:
|
|
|
157
154
|
# All scripts are idempotent — safe to re-run if a step fails.
|
|
158
155
|
|
|
159
156
|
_tasks:
|
|
160
|
-
- "chmod +x .scripts/*.sh .scripts/sentinel/bin/*.sh hermes 2>/dev/null || true"
|
|
157
|
+
- "chmod +x .scripts/*.sh .scripts/sentinel/bin/*.sh hermes momo 2>/dev/null || true"
|
|
161
158
|
- "./.scripts/00-banner.sh"
|
|
162
159
|
- "./.scripts/01-config.sh"
|
|
163
160
|
- "./.scripts/05-fleet-env.sh"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Per-agent runtime — what NOT to commit to the checkpoint repo.
|
|
2
|
+
# Sensitive credentials never go in git.
|
|
3
|
+
.env
|
|
4
|
+
.env.*
|
|
5
|
+
!.env.example
|
|
6
|
+
auth.json
|
|
7
|
+
auth.lock
|
|
8
|
+
secrets.*
|
|
9
|
+
*.pem
|
|
10
|
+
*.key
|
|
11
|
+
gateway.pid
|
|
12
|
+
gateway_state.json
|
|
13
|
+
processes.json
|
|
14
|
+
interrupt_debug.log
|
|
15
|
+
|
|
16
|
+
# Caches (large, regenerable)
|
|
17
|
+
audio_cache/
|
|
18
|
+
image_cache/
|
|
19
|
+
models_dev_cache.json
|
|
20
|
+
context_length_cache.yaml
|
|
21
|
+
honcho.json
|
|
22
|
+
|
|
23
|
+
# Sandboxes (ephemeral execution environments)
|
|
24
|
+
sandboxes/
|
|
25
|
+
|
|
26
|
+
# Heartbeat / sentinel ephemeral state — local to this runtime, never checkpointed.
|
|
27
|
+
# (Keeps the fused checkpoint a true no-op on a clean tree.)
|
|
28
|
+
continuous-ticket-sentinel-state.json
|
|
29
|
+
continuous-ticket-sentinel.lock
|
|
30
|
+
continuous-ticket-sentinel.lock.d/
|
|
31
|
+
.last-checkpoint
|
|
32
|
+
|
|
33
|
+
# Runtime logs (we want the LFS-tracked summary.log if we add one, not the noise)
|
|
34
|
+
logs/*.log
|
|
35
|
+
logs/*.systemd.log
|
|
36
|
+
|
|
37
|
+
# Python cruft
|
|
38
|
+
__pycache__/
|
|
39
|
+
*.pyc
|
|
40
|
+
.scripts/.done-*
|
|
41
|
+
.scripts/.provision.log
|
|
42
|
+
|
|
43
|
+
# Allow these subdirs to exist
|
|
44
|
+
!logs/.gitkeep
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Ensure the distributable config exists before any other step reads it.
|
|
3
3
|
# Seeds ~/.config/hermes-agent-template/config.toml from the shipped example.
|
|
4
|
+
|
|
5
|
+
# MCP render transactions establish repo-local lifecycle eligibility before
|
|
6
|
+
# touching any host-global state. This guard must precede _lib.sh because that
|
|
7
|
+
# library creates the role log and may read fleet configuration.
|
|
8
|
+
if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]; then
|
|
9
|
+
printf '%s\n' '[01] host config — DEFERRED (SKIP_HOST_STATE=1)' >&2
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
4
13
|
# shellcheck source=_lib.sh
|
|
5
14
|
source "$(dirname "$0")/_lib.sh"
|
|
6
15
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Ensure a single shared fleet source-of-truth file exists.
|
|
3
|
+
|
|
4
|
+
if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]; then
|
|
5
|
+
printf '%s\n' '[05] fleet env — DEFERRED (SKIP_HOST_STATE=1)' >&2
|
|
6
|
+
exit 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
3
9
|
# shellcheck source=_lib.sh
|
|
4
10
|
source "$(dirname "$0")/_lib.sh"
|
|
5
11
|
load_role_env
|
|
@@ -8,45 +14,29 @@ already_done 05-fleet-env && log "[05] fleet env already checked — refreshing
|
|
|
8
14
|
|
|
9
15
|
mkdir -p "$(dirname "$FLEET_ENV")"
|
|
10
16
|
|
|
17
|
+
new_fleet_env=0
|
|
11
18
|
if [[ ! -f "$FLEET_ENV" ]]; then
|
|
12
19
|
log "[05] creating fleet source-of-truth: $FLEET_ENV"
|
|
13
|
-
|
|
14
|
-
# Hermes fleet source of truth.
|
|
15
|
-
# All generated wrappers and provisioning scripts read this file.
|
|
16
|
-
HERMES_FLEET_BIN=${HERMES_BIN}
|
|
17
|
-
HERMES_FLEET_REPO=${HERMES_AGENT_REPO}
|
|
18
|
-
HERMES_FLEET_REGISTRY_FILE=${REGISTRY_FILE}
|
|
19
|
-
HERMES_FLEET_OAUTH_FILE=${HERMES_OAUTH_FILE}
|
|
20
|
-
HERMES_FLEET_CODEX_HOME=${CODEX_HOME}
|
|
21
|
-
EOF
|
|
22
|
-
chmod 600 "$FLEET_ENV"
|
|
20
|
+
new_fleet_env=1
|
|
23
21
|
else
|
|
24
22
|
log "[05] fleet env exists: $FLEET_ENV"
|
|
25
23
|
fi
|
|
26
24
|
|
|
25
|
+
fleet_env_parser="$ROLE_DIR/.scripts/lib/parse-fleet-env.py"
|
|
26
|
+
fleet_env_python="$(builtin type -P python3)" \
|
|
27
|
+
|| die "python3 is required to update the fleet source-of-truth"
|
|
28
|
+
|
|
27
29
|
upsert_fleet_env() {
|
|
28
30
|
local key="$1" value="$2"
|
|
29
|
-
|
|
30
|
-
import pathlib
|
|
31
|
-
import sys
|
|
32
|
-
|
|
33
|
-
path = pathlib.Path(sys.argv[1])
|
|
34
|
-
key = sys.argv[2]
|
|
35
|
-
value = sys.argv[3]
|
|
36
|
-
line = f"{key}={value}"
|
|
37
|
-
lines = path.read_text().splitlines() if path.exists() else []
|
|
38
|
-
for idx, existing in enumerate(lines):
|
|
39
|
-
if existing.startswith(f"{key}="):
|
|
40
|
-
lines[idx] = line
|
|
41
|
-
break
|
|
42
|
-
else:
|
|
43
|
-
lines.append(line)
|
|
44
|
-
path.write_text("\n".join(lines) + "\n")
|
|
45
|
-
PYEOF
|
|
31
|
+
"$fleet_env_python" -I "$fleet_env_parser" --upsert "$FLEET_ENV" "$key" "$value"
|
|
46
32
|
}
|
|
47
33
|
|
|
34
|
+
if [[ "$new_fleet_env" == "1" ]]; then
|
|
35
|
+
upsert_fleet_env HERMES_FLEET_BIN "$HERMES_BIN"
|
|
36
|
+
upsert_fleet_env HERMES_FLEET_REPO "$HERMES_AGENT_REPO"
|
|
37
|
+
upsert_fleet_env HERMES_FLEET_REGISTRY_FILE "$REGISTRY_FILE"
|
|
38
|
+
fi
|
|
48
39
|
upsert_fleet_env HERMES_FLEET_OAUTH_FILE "$HERMES_OAUTH_FILE"
|
|
49
40
|
upsert_fleet_env HERMES_FLEET_CODEX_HOME "$CODEX_HOME"
|
|
50
|
-
chmod 600 "$FLEET_ENV"
|
|
51
41
|
|
|
52
42
|
mark_done 05-fleet-env
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
# The directory remains REAL. Step 20 delegates final shared-vs-owned link
|
|
4
4
|
# topology to `pj migrate hermes.runtime-singleton`; no template step may
|
|
5
5
|
# replace the profile itself with a symlink.
|
|
6
|
+
|
|
7
|
+
if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]; then
|
|
8
|
+
printf '%s\n' '[10] Hermes profile — DEFERRED (SKIP_HOST_STATE=1)' >&2
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
6
12
|
# shellcheck source=_lib.sh
|
|
7
13
|
source "$(dirname "$0")/_lib.sh"
|
|
8
14
|
load_role_env
|
|
@@ -108,8 +114,56 @@ PYEOF
|
|
|
108
114
|
fi
|
|
109
115
|
|
|
110
116
|
# Never persist a project-specific terminal.cwd through the named profile.
|
|
111
|
-
#
|
|
112
|
-
#
|
|
117
|
+
# The generated launchers pass TERMINAL_CWD process-locally instead.
|
|
118
|
+
#
|
|
119
|
+
# config.yaml is GENERATED, never hand-written and never a symlink to the fleet
|
|
120
|
+
# base. It is deep_merge(~/.hermes/config.yaml, <profile>/config.delta.yaml).
|
|
121
|
+
# The old symlink-to-base topology was actively harmful: Hermes' atomic writes
|
|
122
|
+
# use os.replace, which REPLACES a symlink with a regular file, so the first
|
|
123
|
+
# in-agent write (/model, onboarding, a config migration) silently detached the
|
|
124
|
+
# profile onto a frozen copy of an old base — and a symlink gave the profile no
|
|
125
|
+
# way to override anything in the first place.
|
|
126
|
+
#
|
|
127
|
+
# Seed an EMPTY delta: a new agent should be identical to the fleet base, and
|
|
128
|
+
# every line here is an override someone must justify later.
|
|
129
|
+
PROFILE_DELTA="$PROFILE_HOME/config.delta.yaml"
|
|
130
|
+
if [[ ! -f "$PROFILE_DELTA" ]]; then
|
|
131
|
+
log " seeding empty config.delta.yaml (override-only SSOT)"
|
|
132
|
+
cat > "$PROFILE_DELTA" <<'DELTA_EOF'
|
|
133
|
+
# Override-only delta for this Hermes profile.
|
|
134
|
+
# Merged over ~/.hermes/config.yaml to produce config.yaml (which is GENERATED).
|
|
135
|
+
# Empty == identical to the fleet base. Add ONLY what must differ.
|
|
136
|
+
{}
|
|
137
|
+
DELTA_EOF
|
|
138
|
+
chmod 600 "$PROFILE_DELTA"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Pin the identity-memory bank explicitly rather than relying on the fleet
|
|
142
|
+
# bank_id_template (agent-{profile}). {profile} resolves through Hermes'
|
|
143
|
+
# get_active_profile_name(), which calls Path.resolve() on HERMES_HOME and
|
|
144
|
+
# requires a lowercase id sitting directly under profiles/. A symlinked profile
|
|
145
|
+
# dir or an uppercase name silently yields the literal "custom" — which would
|
|
146
|
+
# merge this agent's PRIVATE memory into a bank shared with every other agent
|
|
147
|
+
# that also failed to resolve.
|
|
148
|
+
PROFILE_MEM_CFG="$PROFILE_HOME/hindsight/config.json"
|
|
149
|
+
mkdir -p "$(dirname "$PROFILE_MEM_CFG")"
|
|
150
|
+
if [[ ! -f "$PROFILE_MEM_CFG" ]]; then
|
|
151
|
+
log " pinning identity-memory bank: agent-$PROFILE_NAME"
|
|
152
|
+
printf '{\n "bank_id": "agent-%s"\n}\n' "$PROFILE_NAME" > "$PROFILE_MEM_CFG"
|
|
153
|
+
chmod 600 "$PROFILE_MEM_CFG"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# Render config.yaml from base + delta when the renderer is available. Without
|
|
157
|
+
# it the profile still boots (Hermes reads whatever config.yaml exists), but it
|
|
158
|
+
# is not yet under inheritance and `pj audit` will say so.
|
|
159
|
+
PROFILE_RENDERER="${PROFILE_RENDERER:-$HOME/code/33GOD/hermes-agent-template/scripts/hermes-profile-config.py}"
|
|
160
|
+
if [[ -x "$PROFILE_RENDERER" || -f "$PROFILE_RENDERER" ]]; then
|
|
161
|
+
log " rendering config.yaml from fleet base + delta"
|
|
162
|
+
python3 "$PROFILE_RENDERER" render --profile "$PROFILE_NAME" >/dev/null 2>&1 \
|
|
163
|
+
|| warn " render failed; run hermes-profile-config.py render --profile $PROFILE_NAME"
|
|
164
|
+
else
|
|
165
|
+
warn " profile renderer not found at $PROFILE_RENDERER — config.yaml not rendered"
|
|
166
|
+
fi
|
|
113
167
|
|
|
114
168
|
# Canonical shared-skill source of truth + local PM fallback sync.
|
|
115
169
|
CANONICAL_SKILLS_DIR="${CANONICAL_SKILLS_DIR:-$(config_get fleet.canonical_skills_dir '/home/delorenj/.agents/skills')}"
|
|
@@ -117,8 +171,18 @@ CANONICAL_PM_SKILL_SRC="$CANONICAL_SKILLS_DIR/subagent-driven-development"
|
|
|
117
171
|
LOCAL_PM_SKILL_DST="$PROFILE_HOME/skills/software-development/subagent-driven-development"
|
|
118
172
|
|
|
119
173
|
if [[ -d "$CANONICAL_SKILLS_DIR" ]]; then
|
|
120
|
-
|
|
121
|
-
|
|
174
|
+
# skills.external_dirs is a FLEET setting and already lives in
|
|
175
|
+
# ~/.hermes/config.yaml, so every rendered profile inherits it. Writing it
|
|
176
|
+
# per-profile here would (a) be redundant, and (b) write into the GENERATED
|
|
177
|
+
# config.yaml, where the next render discards it — the classic "I set it and
|
|
178
|
+
# it reverted" trap. Verify inheritance instead of re-asserting it.
|
|
179
|
+
if ! env HERMES_HOME="$PROFILE_HOME" "$HERMES_BIN" config get skills.external_dirs 2>/dev/null \
|
|
180
|
+
| grep -qF "$CANONICAL_SKILLS_DIR"; then
|
|
181
|
+
warn " skills.external_dirs does not include $CANONICAL_SKILLS_DIR"
|
|
182
|
+
warn " add it to the FLEET base (~/.hermes/config.yaml), then: hermes-profile-config.py render --all"
|
|
183
|
+
else
|
|
184
|
+
log " skills.external_dirs inherited from fleet base: $CANONICAL_SKILLS_DIR"
|
|
185
|
+
fi
|
|
122
186
|
|
|
123
187
|
# Ensure key PM/local-ops skills are symlinked into runtime/profile skills root.
|
|
124
188
|
# This preserves canonical ownership and keeps updates instant across agents.
|
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
# it becomes the SOT for every agent in this repo.
|
|
13
13
|
# Either way we register this agent under .project.json `agents` and mirror the
|
|
14
14
|
# binding into role.yaml for back-compat (80-registry.sh / 99-summary.sh).
|
|
15
|
+
|
|
16
|
+
# The caller's negative board grant is authoritative. Check it before sourcing
|
|
17
|
+
# _lib.sh so a skipped step cannot load fleet credentials, create logs/markers,
|
|
18
|
+
# inspect bindings, or reach a provider adapter.
|
|
19
|
+
if [[ "${SKIP_PLANE:-0}" == "1" ]]; then
|
|
20
|
+
printf '%s\n' '[42] ticket provider — SKIPPED (SKIP_PLANE=1)' >&2
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
15
24
|
# shellcheck source=_lib.sh
|
|
16
25
|
source "$(dirname "$0")/_lib.sh"
|
|
17
26
|
load_role_env
|
|
@@ -74,7 +83,15 @@ if set_provider == "1":
|
|
|
74
83
|
if board_url: tp["board_url"] = board_url
|
|
75
84
|
if team: tp["team"] = team
|
|
76
85
|
ag = d.setdefault("agents", {})
|
|
77
|
-
|
|
86
|
+
entry = ag.get(os.environ["AGENT_ID"], {})
|
|
87
|
+
if not isinstance(entry, dict):
|
|
88
|
+
entry = {}
|
|
89
|
+
entry.update({
|
|
90
|
+
"role": os.environ["ROLE"],
|
|
91
|
+
"role_dir": os.environ["ROLE_DIR_REL"],
|
|
92
|
+
"provisioning_state": "provisioned",
|
|
93
|
+
})
|
|
94
|
+
ag[os.environ["AGENT_ID"]] = entry
|
|
78
95
|
p.write_text(json.dumps(d, indent=2) + "\n")
|
|
79
96
|
PY
|
|
80
97
|
log " .project.json updated (agent=$AGENT_ID)"
|
|
@@ -37,6 +37,63 @@ if [[ "${SKIP_SYSTEMD:-0}" == "1" ]]; then
|
|
|
37
37
|
exit 0
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
|
+
# Render every caller-controlled systemd scalar through the same data-only
|
|
41
|
+
# serializer used by fleet backfill. This validation happens before mkdir,
|
|
42
|
+
# systemctl, or unit writes, so CR/LF/NUL cannot create a second directive and
|
|
43
|
+
# spaces/quotes/backslashes/specifier bytes remain exact.
|
|
44
|
+
PYTHON_BIN="$(builtin type -P python3)" || die "python3 is unavailable for systemd unit serialization"
|
|
45
|
+
systemd_value() {
|
|
46
|
+
"$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-value "$1" \
|
|
47
|
+
|| die "systemd value validation failed"
|
|
48
|
+
}
|
|
49
|
+
systemd_scalar() {
|
|
50
|
+
"$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-scalar "$1" \
|
|
51
|
+
|| die "systemd scalar validation failed"
|
|
52
|
+
}
|
|
53
|
+
systemd_environment() {
|
|
54
|
+
"$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-environment "$1" "$2" \
|
|
55
|
+
|| die "systemd environment validation failed"
|
|
56
|
+
}
|
|
57
|
+
systemd_exec_value() {
|
|
58
|
+
"$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-exec-value "$1" \
|
|
59
|
+
|| die "systemd ExecStart value validation failed"
|
|
60
|
+
}
|
|
61
|
+
GW_DESCRIPTION="$(systemd_scalar "Hermes Gateway — $DISPLAY_NAME")"
|
|
62
|
+
HB_DESCRIPTION="$(systemd_scalar "Hermes Heartbeat (reconcile + checkpoint) — $DISPLAY_NAME")"
|
|
63
|
+
TIMER_DESCRIPTION="$(systemd_scalar "Heartbeat (reconcile + checkpoint) for $AGENT_ID")"
|
|
64
|
+
ENV_HERMES_HOME="$(systemd_environment HERMES_HOME "$PROFILE_HOME")"
|
|
65
|
+
ENV_HERMES_BIN="$(systemd_environment HERMES_BIN "$HERMES_BIN")"
|
|
66
|
+
ENV_CODEX_HOME="$(systemd_environment CODEX_HOME "$CODEX_HOME")"
|
|
67
|
+
ENV_TERMINAL_CWD="$(systemd_environment TERMINAL_CWD "$REPO_ROOT")"
|
|
68
|
+
WORKING_DIRECTORY="$(systemd_scalar "$REPO_ROOT")"
|
|
69
|
+
RUNTIME_ENV_FILE="$(systemd_scalar "-$RUNTIME/.env")"
|
|
70
|
+
GW_LOG_OUTPUT="$(systemd_scalar "append:$RUNTIME/logs/gateway.systemd.log")"
|
|
71
|
+
HB_LOG_OUTPUT="$(systemd_scalar "append:$RUNTIME/logs/heartbeat.log")"
|
|
72
|
+
GW_EXEC_START="$(systemd_exec_value "$ROLE_DIR/.scripts/credential-launch.sh")"
|
|
73
|
+
HB_EXEC_START="$GW_EXEC_START"
|
|
74
|
+
|
|
75
|
+
# Encrypted credentials are optional and take precedence over the existing
|
|
76
|
+
# ignored runtime/.env fallback. Validate every path-derived unit scalar before
|
|
77
|
+
# any directory, unit, marker, or systemd mutation.
|
|
78
|
+
CREDENTIAL_DIR="${HERMES_SYSTEMD_CREDENTIAL_DIR:-$HOME/.config/hermes-agent/credentials}"
|
|
79
|
+
TELEGRAM_CREDENTIAL="$CREDENTIAL_DIR/${AGENT_ID}-telegram-bot-token.cred"
|
|
80
|
+
MODEL_CREDENTIAL="$CREDENTIAL_DIR/${AGENT_ID}-model-api-key.cred"
|
|
81
|
+
GW_CREDENTIAL_LINES=""
|
|
82
|
+
HB_CREDENTIAL_LINES=""
|
|
83
|
+
if [[ -f "$TELEGRAM_CREDENTIAL" ]]; then
|
|
84
|
+
command -v systemd-creds >/dev/null 2>&1 \
|
|
85
|
+
|| die "encrypted Telegram credential exists but systemd-creds is unavailable"
|
|
86
|
+
GW_CREDENTIAL_LINES="LoadCredentialEncrypted=$(systemd_value "telegram_bot_token:$TELEGRAM_CREDENTIAL")"
|
|
87
|
+
fi
|
|
88
|
+
if [[ -f "$MODEL_CREDENTIAL" ]]; then
|
|
89
|
+
[[ -n "$(yaml_get model.key_env)" ]] \
|
|
90
|
+
|| die "encrypted model credential exists but model.key_env is blank in role.yaml"
|
|
91
|
+
command -v systemd-creds >/dev/null 2>&1 \
|
|
92
|
+
|| die "encrypted model credential exists but systemd-creds is unavailable"
|
|
93
|
+
GW_CREDENTIAL_LINES="${GW_CREDENTIAL_LINES}${GW_CREDENTIAL_LINES:+$'\n'}LoadCredentialEncrypted=$(systemd_value "model_api_key:$MODEL_CREDENTIAL")"
|
|
94
|
+
HB_CREDENTIAL_LINES="LoadCredentialEncrypted=$(systemd_value "model_api_key:$MODEL_CREDENTIAL")"
|
|
95
|
+
fi
|
|
96
|
+
|
|
40
97
|
# Singleton-runtime contract: units set HERMES_HOME to the agent's NAMED PROFILE
|
|
41
98
|
# dir, never the raw runtime path — Hermes derives profile identity and shared
|
|
42
99
|
# fleet auth from the unresolved HERMES_HOME. $RUNTIME stays correct for
|
|
@@ -101,49 +158,26 @@ chmod +x "$HEARTBEAT_BIN" "$CREDENTIAL_LAUNCHER" "$ROLE_DIR/.scripts/checkpoint.
|
|
|
101
158
|
[[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]] \
|
|
102
159
|
|| die "named profile is not a real directory; run: pj migrate hermes.runtime-singleton '$REPO_ROOT'"
|
|
103
160
|
|
|
104
|
-
# Encrypted credentials are optional and take precedence over the existing
|
|
105
|
-
# ignored runtime/.env fallback. Their plaintext appears only below /run while
|
|
106
|
-
# systemd executes the unit. Files are provisioned separately with
|
|
107
|
-
# `systemd-creds encrypt --user`; this step never reads or writes secret values.
|
|
108
|
-
CREDENTIAL_DIR="${HERMES_SYSTEMD_CREDENTIAL_DIR:-$HOME/.config/hermes-agent/credentials}"
|
|
109
|
-
TELEGRAM_CREDENTIAL="$CREDENTIAL_DIR/${AGENT_ID}-telegram-bot-token.cred"
|
|
110
|
-
MODEL_CREDENTIAL="$CREDENTIAL_DIR/${AGENT_ID}-model-api-key.cred"
|
|
111
|
-
GW_CREDENTIAL_LINES=""
|
|
112
|
-
HB_CREDENTIAL_LINES=""
|
|
113
|
-
if [[ -f "$TELEGRAM_CREDENTIAL" ]]; then
|
|
114
|
-
command -v systemd-creds >/dev/null 2>&1 \
|
|
115
|
-
|| die "encrypted Telegram credential exists but systemd-creds is unavailable"
|
|
116
|
-
GW_CREDENTIAL_LINES="LoadCredentialEncrypted=telegram_bot_token:$TELEGRAM_CREDENTIAL"
|
|
117
|
-
fi
|
|
118
|
-
if [[ -f "$MODEL_CREDENTIAL" ]]; then
|
|
119
|
-
[[ -n "$(yaml_get model.key_env)" ]] \
|
|
120
|
-
|| die "encrypted model credential exists but model.key_env is blank in role.yaml"
|
|
121
|
-
command -v systemd-creds >/dev/null 2>&1 \
|
|
122
|
-
|| die "encrypted model credential exists but systemd-creds is unavailable"
|
|
123
|
-
GW_CREDENTIAL_LINES="${GW_CREDENTIAL_LINES}${GW_CREDENTIAL_LINES:+$'\n'}LoadCredentialEncrypted=model_api_key:$MODEL_CREDENTIAL"
|
|
124
|
-
HB_CREDENTIAL_LINES="LoadCredentialEncrypted=model_api_key:$MODEL_CREDENTIAL"
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
161
|
# Gateway unit
|
|
128
162
|
cat > "$SYS_DIR/$GW_UNIT" <<UNIT
|
|
129
163
|
[Unit]
|
|
130
|
-
Description
|
|
164
|
+
Description=$GW_DESCRIPTION
|
|
131
165
|
After=network-online.target
|
|
132
166
|
Wants=network-online.target
|
|
133
167
|
|
|
134
168
|
[Service]
|
|
135
169
|
Type=simple
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
EnvironmentFile
|
|
170
|
+
$ENV_HERMES_HOME
|
|
171
|
+
$ENV_HERMES_BIN
|
|
172
|
+
$ENV_CODEX_HOME
|
|
173
|
+
$ENV_TERMINAL_CWD
|
|
174
|
+
EnvironmentFile=$RUNTIME_ENV_FILE
|
|
141
175
|
$GW_CREDENTIAL_LINES
|
|
142
|
-
ExecStart=$
|
|
176
|
+
ExecStart=$GW_EXEC_START gateway
|
|
143
177
|
Restart=on-failure
|
|
144
178
|
RestartSec=10
|
|
145
|
-
StandardOutput
|
|
146
|
-
StandardError
|
|
179
|
+
StandardOutput=$GW_LOG_OUTPUT
|
|
180
|
+
StandardError=$GW_LOG_OUTPUT
|
|
147
181
|
|
|
148
182
|
[Install]
|
|
149
183
|
WantedBy=default.target
|
|
@@ -155,31 +189,31 @@ UNIT
|
|
|
155
189
|
# The per-agent EnvironmentFiles load ticket-provider creds for the sentinel pass.
|
|
156
190
|
cat > "$SYS_DIR/$HB_SVC" <<UNIT
|
|
157
191
|
[Unit]
|
|
158
|
-
Description
|
|
192
|
+
Description=$HB_DESCRIPTION
|
|
159
193
|
After=network-online.target
|
|
160
194
|
Wants=network-online.target
|
|
161
195
|
|
|
162
196
|
[Service]
|
|
163
197
|
Type=oneshot
|
|
164
|
-
WorkingDirectory=$
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
WorkingDirectory=$WORKING_DIRECTORY
|
|
199
|
+
$ENV_HERMES_HOME
|
|
200
|
+
$ENV_HERMES_BIN
|
|
201
|
+
$ENV_CODEX_HOME
|
|
202
|
+
$ENV_TERMINAL_CWD
|
|
169
203
|
EnvironmentFile=-%h/.config/hermes-agent/env
|
|
170
204
|
EnvironmentFile=-%h/.hermes/env
|
|
171
205
|
EnvironmentFile=-%h/.hermes/hermes-agent.env
|
|
172
206
|
EnvironmentFile=-%h/.hermes/${AGENT_ID}.env
|
|
173
|
-
EnvironmentFile
|
|
207
|
+
EnvironmentFile=$RUNTIME_ENV_FILE
|
|
174
208
|
$HB_CREDENTIAL_LINES
|
|
175
|
-
ExecStart=$
|
|
209
|
+
ExecStart=$HB_EXEC_START heartbeat
|
|
176
210
|
TimeoutStartSec=45min
|
|
177
|
-
StandardOutput
|
|
178
|
-
StandardError
|
|
211
|
+
StandardOutput=$HB_LOG_OUTPUT
|
|
212
|
+
StandardError=$HB_LOG_OUTPUT
|
|
179
213
|
UNIT
|
|
180
214
|
cat > "$SYS_DIR/$HB_TIMER" <<UNIT
|
|
181
215
|
[Unit]
|
|
182
|
-
Description
|
|
216
|
+
Description=$TIMER_DESCRIPTION
|
|
183
217
|
|
|
184
218
|
[Timer]
|
|
185
219
|
OnBootSec=1min
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Append this agent's entry to the global fleet registry.
|
|
3
|
+
|
|
4
|
+
if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]; then
|
|
5
|
+
printf '%s\n' '[80] fleet registry — DEFERRED (SKIP_HOST_STATE=1)' >&2
|
|
6
|
+
exit 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
3
9
|
# shellcheck source=_lib.sh
|
|
4
10
|
source "$(dirname "$0")/_lib.sh"
|
|
5
11
|
load_role_env
|