@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.
Files changed (38) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +4754 -2564
  3. package/dist/mcp-server.js +4516 -2141
  4. package/dist/prompt.js +404 -0
  5. package/package.json +7 -5
  6. package/templates/commonproject/copier.yml +6 -1
  7. package/templates/commonproject/template/.agents/hooks/README.md +10 -10
  8. package/templates/commonproject/template/.agents/hooks/hooks.master.json +1 -1
  9. package/templates/commonproject/template/.agents/hooks/sync.py +4 -4
  10. package/templates/commonproject/template/.agents/local.example.json +1 -1
  11. package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +1 -1
  12. package/templates/commonproject/template/.mise/scripts/provision-packs.py +14 -50
  13. package/templates/commonproject/template/mise.toml.jinja +11 -11
  14. package/templates/hermes-agent/copier.yml +34 -14
  15. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
  16. package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
  17. package/templates/hermes-agent/template/.scripts/01-config.sh +13 -4
  18. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
  19. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +73 -14
  20. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +37 -32
  21. package/templates/hermes-agent/template/.scripts/30-telegram.sh +18 -4
  22. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
  23. package/templates/hermes-agent/template/.scripts/70-systemd.sh +114 -24
  24. package/templates/hermes-agent/template/.scripts/80-registry.sh +19 -3
  25. package/templates/hermes-agent/template/.scripts/_lib.sh +74 -11
  26. package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
  27. package/templates/hermes-agent/template/.scripts/config.example.toml +7 -4
  28. package/templates/hermes-agent/template/.scripts/credential-launch.sh +81 -0
  29. package/templates/hermes-agent/template/.scripts/heartbeat.sh +15 -2
  30. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
  31. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +734 -0
  32. package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
  33. package/templates/hermes-agent/template/.scripts/providers/plane.sh +31 -5
  34. package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
  35. package/templates/hermes-agent/template/SOUL.md.jinja +48 -12
  36. package/templates/hermes-agent/template/hermes.jinja +51 -10
  37. package/templates/hermes-agent/template/momo.jinja +177 -0
  38. 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
- Backwards compatibility: a project that declares no `bmad` pack still gets the
23
- pinned BMAD pack expanded into `skills[]`, exactly as before. Declaring
24
- `{"name": "bmad", ...}` in `packs[]` takes over and the pin is not consulted.
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
- # The BMAD pack pinned when a project declares none. Sealed from this side so
44
- # the pinned release is verified byte-for-byte even though its `pack.toml`
45
- # predates `[policy] sealed`.
46
- BMAD_PACK_NAME = "bmad"
47
- BMAD_PACK_VERSION = "6.10.1-next.31"
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-agentfiles"
42
+ task = "link:agentfiles"
43
43
  [[watch_files]]
44
44
  patterns = [".agents/skills.json"]
45
- task = "skills-sync"
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-sync"
50
+ task = "hooks:sync"
51
51
  {% endif %}
52
- [tasks.link-agentfiles]
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-sync]
56
+ [tasks."skills:sync"]
57
57
  description = "Sync skills from manifest to local CLI dirs"
58
- depends = ["skills-provision-packs"]
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-provision-packs]
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-sync]
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-check]
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-uninstall]
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-setup]
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
- # (this is HERMES_HOME; no project gitlink or runtime remote)
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/.env
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
- choices:
52
- "Project Manager (pm)": pm
53
- "Developer (dev)": dev
54
- "Reviewer (review)": review
55
- "Company Reporter daily cross-project rollup (reporter)": reporter
56
- "Ops (ops)": ops
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 HERMES_HOME for the **{{display_name}}** agent. It is git-tracked
4
- so memory, SOUL evolution, and conversation history are durable across machines
5
- and recoverable on failure.
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` | yes | Inference + skill config (cloned from global at provision time) |
12
- | `SOUL.md` | yes | The agent's personality, evolves over time |
13
- | `memories/MEMORY.md` | yes | The condensed mental-model summary loaded each session |
14
- | `memories/USER.md` | yes | The operator's persona (Jarad DeLorenzo, ...) |
15
- | `sessions/sessions.db` | yes (LFS) | SQLite store of every conversation |
16
- | `decisions/` | yes | Agent-emitted decisions, one file per important call |
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; fleet auth defaults to `HERMES_OAUTH_FILE=~/.hermes/auth.json` |
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 = "~/.hermes/hermes-agent/.venv/bin/hermes"
25
- hermes_repo = "~/.hermes/hermes-agent"
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 = "feature/PJAN-19-routing-publication"
28
- hermes_git_sha = "113e1b182b6d72a7dd02a191f134a41668ceaf0e"
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
- cat > "$FLEET_ENV" <<EOF
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
- python3 - "$FLEET_ENV" "$key" "$value" <<'PYEOF'
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
- # Stage the per-agent runtime config/secrets via a TRANSIENT Hermes profile.
3
- # HERMES_HOME is the ignored ./runtime directory; this profile dir is a staging area
4
- # (cred-stripped .env + SOUL) that 20-runtime-repo.sh folds into the runtime and
5
- # then replaces with a symlink ~/.hermes/profiles/<name> -> ./runtime. That
6
- # symlink is load-bearing: hermes resolves `--profile <name>` invocations
7
- # through it (recreating it wrongly if it goes missing).
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
- # Apply role-specific config overrides.
116
- log " setting terminal.cwd = $REPO_PATH"
117
- env HERMES_HOME="$PROFILE_HOME" "$HERMES_BIN" config set terminal.cwd "$REPO_PATH"
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
- log " setting skills.external_dirs[0] = $CANONICAL_SKILLS_DIR"
126
- env HERMES_HOME="$PROFILE_HOME" "$HERMES_BIN" config set skills.external_dirs.0 "$CANONICAL_SKILLS_DIR"
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 && { log "[20] local runtime already set up — skipping"; exit 0; }
10
- [[ "${SKIP_RUNTIME_REPO:-0}" == "1" ]] && { log "[20] local runtime SKIPPED (SKIP_RUNTIME_REPO=1)"; mark_done 20-runtime-repo; exit 0; }
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
- # Fold the two supported staging-profile files into the runtime without
127
- # deleting unknown profile content. Unknown content blocks the symlink so an
128
- # operator can reconcile it explicitly instead of losing state.
129
- if [[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]]; then
130
- log " migrating supported staging profile state into the local runtime"
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
- ln -sfn "$RUNTIME_LOCAL" "$PROFILE_HOME"
147
- log " profile symlink $PROFILE_HOME -> $RUNTIME_LOCAL"
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
- env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set terminal.cwd "$PROJECT_PATH" >/dev/null 2>&1 || true
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
- if [[ -x "$HERMES_BIN" ]]; then
162
- env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set plugins.enabled.0 tts/voxxy >/dev/null 2>&1 || true
163
- env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set tts.provider voxxy >/dev/null 2>&1 || true
164
- env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set tts.voice rick >/dev/null 2>&1 || true
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