@delorenj/pjangler 1.2.33 → 1.3.7
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/README.md +72 -0
- package/dist/index.js +4754 -2564
- package/dist/mcp-server.js +4516 -2141
- package/dist/prompt.js +404 -0
- package/package.json +7 -5
- package/templates/commonproject/copier.yml +6 -1
- package/templates/commonproject/template/.agents/hooks/README.md +10 -10
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +1 -1
- package/templates/commonproject/template/.agents/hooks/sync.py +4 -4
- package/templates/commonproject/template/.agents/local.example.json +1 -1
- package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +1 -1
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +14 -50
- package/templates/commonproject/template/mise.toml.jinja +11 -11
- package/templates/hermes-agent/copier.yml +34 -14
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
- package/templates/hermes-agent/template/.scripts/01-config.sh +13 -4
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +73 -14
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +37 -32
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +18 -4
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +114 -24
- package/templates/hermes-agent/template/.scripts/80-registry.sh +19 -3
- package/templates/hermes-agent/template/.scripts/_lib.sh +74 -11
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
- package/templates/hermes-agent/template/.scripts/config.example.toml +7 -4
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +81 -0
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +15 -2
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +734 -0
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +31 -5
- package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
- package/templates/hermes-agent/template/SOUL.md.jinja +48 -12
- package/templates/hermes-agent/template/hermes.jinja +51 -10
- package/templates/hermes-agent/template/momo.jinja +177 -0
- package/templates/hermes-agent/template/role.yaml.jinja +31 -21
|
@@ -19,9 +19,10 @@ What this script owns is the transactional projection into the project:
|
|
|
19
19
|
one unsafe or tampered pack produces ZERO mutation
|
|
20
20
|
* `.agents/skills.json` is rewritten atomically, preserving its mode
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
Only packs a project DECLARES in `packs[]` are provisioned. Nothing is pinned
|
|
23
|
+
implicitly -- in particular BMAD is not a pack: `bmad-method install` writes
|
|
24
|
+
bmad-* skills into `.agents/skills` itself, versioned per project by
|
|
25
|
+
`_bmad/_config/manifest.yaml`.
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
28
|
from __future__ import annotations
|
|
@@ -40,11 +41,12 @@ from typing import Callable
|
|
|
40
41
|
SKILLS_SCHEMA = "https://raw.githubusercontent.com/delorenj/skillex/main/skills.schema.json"
|
|
41
42
|
SKILLS_REGISTRY = "https://github.com/delorenj/skillex.git"
|
|
42
43
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
# BMAD is NOT a pack. `bmad-method install` writes bmad-* skills into
|
|
45
|
+
# .agents/skills itself, per project, versioned by _bmad/_config/manifest.yaml.
|
|
46
|
+
# This script used to pin a frozen `packs/bmad/<version>` in the registry and
|
|
47
|
+
# project a second copy of the same skills; when the registry dropped that pack
|
|
48
|
+
# the pin took every `pjangler project create` down with it on any machine
|
|
49
|
+
# without a warm cache. Only packs a project DECLARES are provisioned now.
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
def load_engine():
|
|
@@ -68,38 +70,13 @@ engine = load_engine()
|
|
|
68
70
|
|
|
69
71
|
|
|
70
72
|
def pack_root_override(name: str) -> Path | None:
|
|
71
|
-
"""Developer/test pin for one pack root, e.g. `PJ_PACK_ROOT_HERMES_BASE`.
|
|
72
|
-
|
|
73
|
-
`PJ_BMAD_PACK_ROOT` predates the generic form and stays a first-class alias
|
|
74
|
-
for the `bmad` pack.
|
|
75
|
-
"""
|
|
73
|
+
"""Developer/test pin for one pack root, e.g. `PJ_PACK_ROOT_HERMES_BASE`."""
|
|
76
74
|
generic = os.environ.get(f"PJ_PACK_ROOT_{re.sub(r'[^A-Z0-9]', '_', name.upper())}", "").strip()
|
|
77
75
|
if generic:
|
|
78
76
|
return Path(generic).expanduser().absolute()
|
|
79
|
-
if name == BMAD_PACK_NAME:
|
|
80
|
-
legacy = os.environ.get("PJ_BMAD_PACK_ROOT", "").strip()
|
|
81
|
-
if legacy:
|
|
82
|
-
return Path(legacy).expanduser().absolute()
|
|
83
77
|
return None
|
|
84
78
|
|
|
85
79
|
|
|
86
|
-
def implicit_bmad_entry() -> dict:
|
|
87
|
-
"""The implicit BMAD pin, expressed as an ordinary `packs[]` entry.
|
|
88
|
-
|
|
89
|
-
It deliberately carries NO `source`: like every declared pack it must walk
|
|
90
|
-
the one resolution ladder in `resolve_pack_root()` -- env override first,
|
|
91
|
-
then the contract-ordered registry checkouts. Pinning a hardcoded root here
|
|
92
|
-
would give the same pack name two resolutions in one process, so adding
|
|
93
|
-
`packs:[{"name":"bmad",...}]` to a manifest would silently MOVE the pack.
|
|
94
|
-
"""
|
|
95
|
-
return {
|
|
96
|
-
"name": BMAD_PACK_NAME,
|
|
97
|
-
"version": BMAD_PACK_VERSION,
|
|
98
|
-
"sealed": True,
|
|
99
|
-
"optional": False,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
80
|
def apply_root_override(entry: dict) -> dict:
|
|
104
81
|
"""Pin a declared pack at an overridden root without changing its identity."""
|
|
105
82
|
if entry.get("source") or entry.get("registry_path"):
|
|
@@ -150,24 +127,11 @@ def resolve_declared_packs(manifest: dict, base_dir: Path):
|
|
|
150
127
|
# Later packs override earlier ones (contract section 5).
|
|
151
128
|
declared_members[name] = path
|
|
152
129
|
|
|
130
|
+
# Nothing is pinned implicitly any more. The empty pair is kept so every
|
|
131
|
+
# caller keeps one shape and the manifest writer still evicts leftovers
|
|
132
|
+
# from when something WAS pinned here.
|
|
153
133
|
implicit_members: dict[str, Path] = {}
|
|
154
134
|
implicit_packs: list[dict] = []
|
|
155
|
-
if not any(entry["name"] == BMAD_PACK_NAME for entry in entries):
|
|
156
|
-
# Same call shape as a declared pack above, so the pin cannot resolve
|
|
157
|
-
# anywhere a declared `bmad` entry would not.
|
|
158
|
-
pinned = engine.normalize_pack_entry(apply_root_override(implicit_bmad_entry()))
|
|
159
|
-
pinned["sealed"] = True
|
|
160
|
-
for name, path in engine.resolve_pack(
|
|
161
|
-
pinned,
|
|
162
|
-
cache_dir,
|
|
163
|
-
base_dir,
|
|
164
|
-
default_registry,
|
|
165
|
-
registry_roots,
|
|
166
|
-
managed_roots,
|
|
167
|
-
on_resolved=implicit_packs.append,
|
|
168
|
-
):
|
|
169
|
-
implicit_members[name] = path
|
|
170
|
-
|
|
171
135
|
return declared_members, implicit_members, declared_packs, implicit_packs
|
|
172
136
|
|
|
173
137
|
|
|
@@ -39,43 +39,43 @@ script = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --uninsta
|
|
|
39
39
|
|
|
40
40
|
[[watch_files]]
|
|
41
41
|
patterns = ["AGENTS.md"]
|
|
42
|
-
task = "link
|
|
42
|
+
task = "link:agentfiles"
|
|
43
43
|
[[watch_files]]
|
|
44
44
|
patterns = [".agents/skills.json"]
|
|
45
|
-
task = "skills
|
|
45
|
+
task = "skills:sync"
|
|
46
46
|
{% if agent_hooks_layer %}
|
|
47
47
|
# Re-fan-out the agent hooks whenever the single source of truth changes.
|
|
48
48
|
[[watch_files]]
|
|
49
49
|
patterns = [".agents/hooks/hooks.master.json"]
|
|
50
|
-
task = "hooks
|
|
50
|
+
task = "hooks:sync"
|
|
51
51
|
{% endif %}
|
|
52
|
-
[tasks.link
|
|
52
|
+
[tasks."link:agentfiles"]
|
|
53
53
|
description = "Symlink all agent files to AGENTS.md"
|
|
54
54
|
run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
|
|
55
55
|
|
|
56
|
-
[tasks.skills
|
|
56
|
+
[tasks."skills:sync"]
|
|
57
57
|
description = "Sync skills from manifest to local CLI dirs"
|
|
58
|
-
depends = ["skills
|
|
58
|
+
depends = ["skills:provision:packs"]
|
|
59
59
|
run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project"
|
|
60
60
|
|
|
61
|
-
[tasks.skills
|
|
61
|
+
[tasks."skills:provision:packs"]
|
|
62
62
|
description = "Provision every Skillex pack declared in .agents/skills.json"
|
|
63
63
|
run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py'"
|
|
64
64
|
{% if agent_hooks_layer %}
|
|
65
65
|
# --- Project-scoped agent hooks + skill fan-out (see .agents/hooks/README.md) ---
|
|
66
66
|
|
|
67
|
-
[tasks.hooks
|
|
67
|
+
[tasks."hooks:sync"]
|
|
68
68
|
description = "Fan out .agents/hooks/hooks.master.json to each agent CLI (claude committed; codex/kimi injected; hermes adapter)"
|
|
69
69
|
run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --install"
|
|
70
70
|
|
|
71
|
-
[tasks.hooks
|
|
71
|
+
[tasks."hooks:check"]
|
|
72
72
|
description = "Drift gate: verify generated hook configs match hooks.master.json (CI-safe, read-only)"
|
|
73
73
|
run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --check"
|
|
74
74
|
|
|
75
|
-
[tasks.hooks
|
|
75
|
+
[tasks."hooks:uninstall"]
|
|
76
76
|
description = "Remove per-user agent-hook injections (codex/kimi/hermes)"
|
|
77
77
|
run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --uninstall"
|
|
78
78
|
{% endif %}
|
|
79
|
-
[tasks.hindsight
|
|
79
|
+
[tasks."hindsight:setup"]
|
|
80
80
|
description = "Provision this dev's shared project Hindsight key from 1Password into .env (op read; upserts those two keys, leaves the rest of .env intact)"
|
|
81
81
|
run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/hindsight-setup.sh'"
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
#
|
|
11
11
|
# What it does:
|
|
12
12
|
# 1. Renders agents/hermes/<role>/{role.yaml, SOUL.md, hermes, .scripts/}
|
|
13
|
-
# 2. Creates ignored local state at agents/hermes/<role>/runtime/
|
|
14
|
-
#
|
|
13
|
+
# 2. Creates ignored local state at agents/hermes/<role>/runtime/ and delegates
|
|
14
|
+
# named-profile wiring to `pj migrate hermes.runtime-singleton`
|
|
15
15
|
# 4. Creates a Plane project in 33god workspace
|
|
16
|
-
# 6. Prompts for a BotFather Telegram token, stores in runtime
|
|
16
|
+
# 6. Prompts for a BotFather Telegram token, stores in ignored runtime state
|
|
17
17
|
# 7. Optionally wires a dedicated Slack app+bot pair into runtime/.env
|
|
18
18
|
# 8. Records fleet-shared Bloodbank routing identity (no profile consumer)
|
|
19
19
|
# 9. Installs a profile gateway + heartbeat timer (reconcile + checkpoint)
|
|
@@ -46,16 +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
|
-
"QA (qa)": qa
|
|
58
|
-
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 %}
|
|
59
57
|
|
|
60
58
|
ticket_provider:
|
|
61
59
|
type: str
|
|
@@ -69,7 +67,7 @@ ticket_provider:
|
|
|
69
67
|
agent_purpose:
|
|
70
68
|
type: str
|
|
71
69
|
help: "One-line description of what this agent does in this repo"
|
|
72
|
-
default: "{% if role == 'reporter' %}Company-wide evidence gathering, freshness-aware daily reporting, and critical delivery monitoring{% else %}Project management, triage, and continuous board reconciliation{% endif %}"
|
|
70
|
+
default: "{% if role == 'reporter' %}Company-wide evidence gathering, freshness-aware daily reporting, and critical delivery monitoring{% elif role == 'director' %}Company-level portfolio orchestration, delegation, and cross-project control{% else %}Project management, triage, and continuous board reconciliation{% endif %}"
|
|
73
71
|
|
|
74
72
|
model_provider:
|
|
75
73
|
type: str
|
|
@@ -81,6 +79,28 @@ model_name:
|
|
|
81
79
|
help: "Model name (empty = inherit from global ~/.hermes config)"
|
|
82
80
|
default: ""
|
|
83
81
|
|
|
82
|
+
model_base_url:
|
|
83
|
+
type: str
|
|
84
|
+
help: "Per-agent model API base URL (empty = inherit from shared config)"
|
|
85
|
+
default: ""
|
|
86
|
+
|
|
87
|
+
model_api_mode:
|
|
88
|
+
type: str
|
|
89
|
+
help: "Per-agent model API protocol (empty = inherit from shared config)"
|
|
90
|
+
default: ""
|
|
91
|
+
choices:
|
|
92
|
+
"Inherit from shared config": ""
|
|
93
|
+
"OpenAI-compatible chat completions": chat_completions
|
|
94
|
+
"OpenAI Codex responses": codex_responses
|
|
95
|
+
"Anthropic messages": anthropic_messages
|
|
96
|
+
"AWS Bedrock converse": bedrock_converse
|
|
97
|
+
"Codex app server": codex_app_server
|
|
98
|
+
|
|
99
|
+
model_key_env:
|
|
100
|
+
type: str
|
|
101
|
+
help: "Environment variable NAME holding the per-agent model key (never the key value)"
|
|
102
|
+
default: ""
|
|
103
|
+
|
|
84
104
|
soul_tone:
|
|
85
105
|
type: str
|
|
86
106
|
help: "Tone for the agent's SOUL.md"
|
|
@@ -134,7 +154,7 @@ plane_workspace:
|
|
|
134
154
|
# All scripts are idempotent — safe to re-run if a step fails.
|
|
135
155
|
|
|
136
156
|
_tasks:
|
|
137
|
-
- "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"
|
|
138
158
|
- "./.scripts/00-banner.sh"
|
|
139
159
|
- "./.scripts/01-config.sh"
|
|
140
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,21 +1,22 @@
|
|
|
1
1
|
# {{agent_id}} — runtime
|
|
2
2
|
|
|
3
|
-
This is the
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This is the ignored, role-owned state for the **{{display_name}}** agent. The
|
|
4
|
+
actual HERMES_HOME is a real named directory under `~/.hermes/profiles/`, with
|
|
5
|
+
PJangler-managed links into this runtime. Back it up separately; it is not
|
|
6
|
+
published by project Git.
|
|
6
7
|
|
|
7
8
|
## What's in here
|
|
8
9
|
|
|
9
10
|
| Path | Tracked? | Purpose |
|
|
10
11
|
| --- | --- | --- |
|
|
11
|
-
| `config.yaml` |
|
|
12
|
-
| `SOUL.md` |
|
|
13
|
-
| `memories/MEMORY.md` |
|
|
14
|
-
| `memories/USER.md` |
|
|
15
|
-
| `sessions/sessions.db` |
|
|
16
|
-
| `decisions/` |
|
|
12
|
+
| `config.yaml` | local | Legacy seed/delta; shared profile config comes from fleet root |
|
|
13
|
+
| `SOUL.md` | local | The agent's personality, evolves over time |
|
|
14
|
+
| `memories/MEMORY.md` | local | The condensed mental-model summary loaded each session |
|
|
15
|
+
| `memories/USER.md` | local | The operator's persona |
|
|
16
|
+
| `sessions/sessions.db` | local | SQLite store of conversations |
|
|
17
|
+
| `decisions/` | local | Agent-emitted decisions |
|
|
17
18
|
| `.env` | **no** | API keys + Telegram bot token (per-machine secret) |
|
|
18
|
-
| `auth.json` | **no** | Deprecated local OAuth store;
|
|
19
|
+
| `auth.json` | **no** | Deprecated local OAuth store; named profiles use fleet auth fallback |
|
|
19
20
|
| `audio_cache/`, `image_cache/` | **no** | Regenerable caches |
|
|
20
21
|
| `sandboxes/` | **no** | Per-session ephemeral execution dirs |
|
|
21
22
|
|
|
@@ -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
|
|
|
@@ -21,11 +30,11 @@ else
|
|
|
21
30
|
cat > "$CONFIG" <<'TOML'
|
|
22
31
|
# hermes-agent-template config — edit for your environment.
|
|
23
32
|
[fleet]
|
|
24
|
-
hermes_bin = "~/.
|
|
25
|
-
hermes_repo = "~/.
|
|
33
|
+
hermes_bin = "~/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1/.venv/bin/hermes"
|
|
34
|
+
hermes_repo = "~/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1"
|
|
26
35
|
hermes_git_url = "https://github.com/delorenj/hermes-agent.git"
|
|
27
|
-
hermes_git_ref = "
|
|
28
|
-
hermes_git_sha = "
|
|
36
|
+
hermes_git_ref = "main"
|
|
37
|
+
hermes_git_sha = "0408fec7a153e6c32c064acd2b8053917f1525f1"
|
|
29
38
|
fleet_env = "~/.hermes/fleet.env"
|
|
30
39
|
registry_file = "~/.hermes/agents-registry.yaml"
|
|
31
40
|
canonical_skills_dir = "/home/delorenj/.agents/skills"
|
|
@@ -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
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
# Create the initial named Hermes profile and sanitize cloned chat credentials.
|
|
3
|
+
# The directory remains REAL. Step 20 delegates final shared-vs-owned link
|
|
4
|
+
# topology to `pj migrate hermes.runtime-singleton`; no template step may
|
|
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
|
+
|
|
8
12
|
# shellcheck source=_lib.sh
|
|
9
13
|
source "$(dirname "$0")/_lib.sh"
|
|
10
14
|
load_role_env
|
|
@@ -14,9 +18,6 @@ already_done 10-hermes-profile && { log "[10] profile already created — skippi
|
|
|
14
18
|
log "[10] creating hermes profile: $PROFILE_NAME"
|
|
15
19
|
PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
|
|
16
20
|
|
|
17
|
-
# Set repo path to directory where .git is
|
|
18
|
-
REPO_PATH="$(cd "$ROLE_DIR" && git rev-parse --show-toplevel 2>/dev/null || echo "$ROLE_DIR")"
|
|
19
|
-
|
|
20
21
|
if [[ -d "$PROFILE_HOME" ]]; then
|
|
21
22
|
log " profile dir already exists; reusing"
|
|
22
23
|
else
|
|
@@ -112,9 +113,57 @@ PYEOF
|
|
|
112
113
|
fi
|
|
113
114
|
fi
|
|
114
115
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
# Never persist a project-specific terminal.cwd through the named profile.
|
|
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
|
|
118
167
|
|
|
119
168
|
# Canonical shared-skill source of truth + local PM fallback sync.
|
|
120
169
|
CANONICAL_SKILLS_DIR="${CANONICAL_SKILLS_DIR:-$(config_get fleet.canonical_skills_dir '/home/delorenj/.agents/skills')}"
|
|
@@ -122,8 +171,18 @@ CANONICAL_PM_SKILL_SRC="$CANONICAL_SKILLS_DIR/subagent-driven-development"
|
|
|
122
171
|
LOCAL_PM_SKILL_DST="$PROFILE_HOME/skills/software-development/subagent-driven-development"
|
|
123
172
|
|
|
124
173
|
if [[ -d "$CANONICAL_SKILLS_DIR" ]]; then
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
127
186
|
|
|
128
187
|
# Ensure key PM/local-ops skills are symlinked into runtime/profile skills root.
|
|
129
188
|
# This preserves canonical ownership and keeps updates instant across agents.
|
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
source "$(dirname "$0")/_lib.sh"
|
|
7
7
|
load_role_env
|
|
8
8
|
|
|
9
|
-
already_done 20-runtime-repo
|
|
10
|
-
|
|
9
|
+
already_done 20-runtime-repo \
|
|
10
|
+
&& log "[20] local runtime scaffold already set up — re-auditing singleton profile wiring"
|
|
11
|
+
if [[ "${SKIP_RUNTIME_REPO:-0}" == "1" ]]; then
|
|
12
|
+
clear_done 20-runtime-repo
|
|
13
|
+
log "[20] local runtime — DEFERRED (SKIP_RUNTIME_REPO=1; completion marker cleared)"
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
11
16
|
|
|
12
17
|
PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
|
|
13
18
|
RUNTIME_LOCAL="$ROLE_DIR/runtime"
|
|
@@ -123,30 +128,34 @@ if [[ ! -e "$RUNTIME_LOCAL/SOUL.md" ]]; then
|
|
|
123
128
|
cp "$ROLE_DIR/SOUL.md" "$RUNTIME_LOCAL/SOUL.md"
|
|
124
129
|
fi
|
|
125
130
|
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
if [[
|
|
130
|
-
|
|
131
|
-
# A reporter never inherits .env; its credentials must be provisioned
|
|
132
|
-
# explicitly against a bot identity it owns.
|
|
133
|
-
if [[ "$ROLE" == "reporter" ]]; then
|
|
134
|
-
MIGRATE_FILES=(config.yaml profile.yaml)
|
|
135
|
-
else
|
|
136
|
-
MIGRATE_FILES=(.env config.yaml)
|
|
137
|
-
fi
|
|
138
|
-
for file_name in "${MIGRATE_FILES[@]}"; do
|
|
139
|
-
[[ -f "$PROFILE_HOME/$file_name" && ! -e "$RUNTIME_LOCAL/$file_name" ]] && cp "$PROFILE_HOME/$file_name" "$RUNTIME_LOCAL/$file_name"
|
|
140
|
-
[[ -f "$PROFILE_HOME/$file_name" ]] && rm -f -- "$PROFILE_HOME/$file_name"
|
|
141
|
-
done
|
|
142
|
-
if ! rmdir "$PROFILE_HOME" 2>/dev/null; then
|
|
143
|
-
die "staging profile contains unrecognized state and was preserved: $PROFILE_HOME"
|
|
144
|
-
fi
|
|
131
|
+
# Named-profile topology belongs exclusively to PJangler. Preview first, then
|
|
132
|
+
# apply the one idempotent rule. This script never removes or replaces a real
|
|
133
|
+
# profile directory and never creates the obsolete profile -> runtime symlink.
|
|
134
|
+
if [[ "$PJANGLER_BIN" != */* ]]; then
|
|
135
|
+
PJANGLER_BIN="$(command -v "$PJANGLER_BIN" 2>/dev/null || true)"
|
|
145
136
|
fi
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
[[ -n "$PJANGLER_BIN" && -x "$PJANGLER_BIN" ]] \
|
|
138
|
+
|| die "PJángler CLI not found; install 'pj' or set PJANGLER_BIN"
|
|
139
|
+
"$PJANGLER_BIN" migrate hermes.runtime-singleton "$PROJECT_PATH" --dry-run --json >/dev/null \
|
|
140
|
+
|| die "singleton-runtime audit failed; profile was left untouched"
|
|
141
|
+
"$PJANGLER_BIN" migrate hermes.runtime-singleton "$PROJECT_PATH" --json >/dev/null \
|
|
142
|
+
|| die "singleton-runtime migration failed; inspect with: pj migrate hermes.runtime-singleton '$PROJECT_PATH' --dry-run"
|
|
143
|
+
[[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]] \
|
|
144
|
+
|| die "PJángler did not establish a real named profile at $PROFILE_HOME"
|
|
145
|
+
log " singleton profile verified by pj migrate hermes.runtime-singleton: $PROFILE_HOME"
|
|
148
146
|
|
|
149
|
-
|
|
147
|
+
# Never persist a project-specific terminal.cwd through the named profile.
|
|
148
|
+
# config.yaml is fleet-shared in the singleton topology. The manual and
|
|
149
|
+
# service launchers pass TERMINAL_CWD process-locally for this role instead.
|
|
150
|
+
|
|
151
|
+
profile_config_set() {
|
|
152
|
+
local key="$1"
|
|
153
|
+
[[ -x "$HERMES_BIN" ]] \
|
|
154
|
+
|| die "Hermes CLI is not executable; cannot configure named profile: $HERMES_BIN"
|
|
155
|
+
if ! env HERMES_HOME="$PROFILE_HOME" "$HERMES_BIN" config set "$@" >/dev/null; then
|
|
156
|
+
die "required Hermes config write failed for named profile $PROFILE_NAME: $key"
|
|
157
|
+
fi
|
|
158
|
+
}
|
|
150
159
|
|
|
151
160
|
if [[ "$ROLE" == "pm" ]]; then
|
|
152
161
|
VOXXY_PLUGIN_DIR="${VOXXY_PLUGIN_DIR:-$(config_get fleet.voxxy_plugin_dir "$HOME/code/voxxy/plugins/tts/voxxy")}"
|
|
@@ -158,14 +167,10 @@ if [[ "$ROLE" == "pm" ]]; then
|
|
|
158
167
|
warn " Voxxy plugin dir missing: $VOXXY_PLUGIN_DIR"
|
|
159
168
|
fi
|
|
160
169
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
log " set PM runtime TTS provider -> voxxy"
|
|
166
|
-
else
|
|
167
|
-
warn " Hermes bin missing; skipped PM Voxxy config"
|
|
168
|
-
fi
|
|
170
|
+
profile_config_set plugins.enabled.0 tts/voxxy
|
|
171
|
+
profile_config_set tts.provider voxxy
|
|
172
|
+
profile_config_set tts.voice rick
|
|
173
|
+
log " set PM named-profile TTS provider -> voxxy"
|
|
169
174
|
fi
|
|
170
175
|
|
|
171
176
|
mark_done 20-runtime-repo
|