@delorenj/pjangler 1.2.33 → 1.3.0

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 (26) hide show
  1. package/dist/index.js +295 -130
  2. package/dist/mcp-server.js +297 -129
  3. package/package.json +1 -1
  4. package/templates/commonproject/template/.agents/hooks/README.md +10 -10
  5. package/templates/commonproject/template/.agents/hooks/hooks.master.json +1 -1
  6. package/templates/commonproject/template/.agents/hooks/sync.py +4 -4
  7. package/templates/commonproject/template/.agents/local.example.json +1 -1
  8. package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +1 -1
  9. package/templates/commonproject/template/mise.toml.jinja +11 -11
  10. package/templates/hermes-agent/copier.yml +27 -4
  11. package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
  12. package/templates/hermes-agent/template/.scripts/01-config.sh +4 -4
  13. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +7 -12
  14. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +37 -32
  15. package/templates/hermes-agent/template/.scripts/30-telegram.sh +18 -4
  16. package/templates/hermes-agent/template/.scripts/70-systemd.sh +72 -12
  17. package/templates/hermes-agent/template/.scripts/80-registry.sh +13 -3
  18. package/templates/hermes-agent/template/.scripts/_lib.sh +12 -5
  19. package/templates/hermes-agent/template/.scripts/config.example.toml +7 -4
  20. package/templates/hermes-agent/template/.scripts/credential-launch.sh +81 -0
  21. package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -1
  22. package/templates/hermes-agent/template/.scripts/providers/plane.sh +30 -4
  23. package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
  24. package/templates/hermes-agent/template/SOUL.md.jinja +4 -4
  25. package/templates/hermes-agent/template/hermes.jinja +36 -7
  26. package/templates/hermes-agent/template/role.yaml.jinja +15 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.2.33",
3
+ "version": "1.3.0",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,11 +12,11 @@ master writes zero bytes).
12
12
  ## TL;DR
13
13
 
14
14
  ```bash
15
- mise run hooks-sync # fan out the master -> claude + codex + hermes
16
- mise run hooks-check # drift gate (read-only; used in CI)
17
- mise run hooks-uninstall # remove codex + hermes injections
18
- mise run skills-sync # sync .agents/skills.json -> local CLI skill dirs
19
- mise run hindsight-setup # one-time: pull the shared CAF Hindsight key into .env
15
+ mise run hooks:sync # fan out the master -> claude + codex + hermes
16
+ mise run hooks:check # drift gate (read-only; used in CI)
17
+ mise run hooks:uninstall # remove codex + hermes injections
18
+ mise run skills:sync # sync .agents/skills.json -> local CLI skill dirs
19
+ mise run hindsight:setup # one-time: pull the shared CAF Hindsight key into .env
20
20
  ```
21
21
 
22
22
  You normally run nothing — `mise` does it on directory enter/leave (see
@@ -93,10 +93,10 @@ merged in automatically.
93
93
 
94
94
  - Project-local skills with the same name shadow the global ones.
95
95
  - No `defer_to_global` flag or bash cleanup scripts are needed anymore.
96
- - The manifest is the only hand-edited skill SSOT; run `mise run skills-sync`
96
+ - The manifest is the only hand-edited skill SSOT; run `mise run skills:sync`
97
97
  after changing it, or just re-enter the repo.
98
98
 
99
- ## Hindsight credentials — `mise run hindsight-setup`
99
+ ## Hindsight credentials — `mise run hindsight:setup`
100
100
 
101
101
  The hooks shell out to `hindsight`, which needs an API key for the self-hosted
102
102
  instance. Strategy: **one shared CAF-scoped key in 1Password**. `hindsight-setup`
@@ -105,7 +105,7 @@ gitignored `.env` (mise loads it, env vars outrank `~/.hindsight/config`). Point
105
105
  it at the right item if the default ref is wrong:
106
106
 
107
107
  ```bash
108
- HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight-setup
108
+ HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight:setup
109
109
  ```
110
110
 
111
111
  Without a key the hooks **no-op gracefully** — recall/retain just do nothing.
@@ -113,10 +113,10 @@ Without a key the hooks **no-op gracefully** — recall/retain just do nothing.
113
113
  ## Adding / changing a hook
114
114
 
115
115
  1. Edit `hooks.master.json`.
116
- 2. `mise run hooks-sync`.
116
+ 2. `mise run hooks:sync`.
117
117
  3. Commit the master **and** the regenerated `.claude/settings.json` together
118
118
  (codex/hermes targets are per-dev/per-deployment, not committed).
119
- 4. CI runs `mise run hooks-check` — fails if the committed Claude settings drift.
119
+ 4. CI runs `mise run hooks:check` — fails if the committed Claude settings drift.
120
120
 
121
121
  ## Files
122
122
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "SINGLE SOURCE OF TRUTH for this repo's project-scoped agent hooks. Hand-edit ONLY this file, then run `mise run hooks-sync` (or re-cd into the repo) to regenerate every per-agent config. Generated files are NOT hand-edited. Pattern: the /ssot-fanout skill. See .agents/hooks/README.md.",
2
+ "$comment": "SINGLE SOURCE OF TRUTH for this repo's project-scoped agent hooks. Hand-edit ONLY this file, then run `mise run hooks:sync` (or re-cd into the repo) to regenerate every per-agent config. Generated files are NOT hand-edited. Pattern: the /ssot-fanout skill. See .agents/hooks/README.md.",
3
3
  "version": 1,
4
4
  "marker": "project-agent-hooks",
5
5
  "hooks": [
@@ -515,7 +515,7 @@ def cmd_check(master: dict) -> int:
515
515
  current = target.read_text() if target.exists() else None
516
516
  if current != desired:
517
517
  warn(f"DRIFT: {target.relative_to(REPO_ROOT)} differs from hooks.master.json "
518
- f"(run `mise run hooks-sync`)")
518
+ f"(run `mise run hooks:sync`)")
519
519
  rc = 1
520
520
  else:
521
521
  log(f"claude: in sync ({target.relative_to(REPO_ROOT)})")
@@ -538,7 +538,7 @@ def cmd_check(master: dict) -> int:
538
538
  ]
539
539
  if missing:
540
540
  warn(f"DRIFT: codex hooks.json missing {len(missing)} project hook(s) "
541
- f"(run `mise run hooks-sync`)")
541
+ f"(run `mise run hooks:sync`)")
542
542
  rc = 1
543
543
  else:
544
544
  log(f"codex: in sync ({ct})")
@@ -558,7 +558,7 @@ def cmd_check(master: dict) -> int:
558
558
  missing = [c for c in want_cmds if c not in present]
559
559
  if begin not in present or missing:
560
560
  warn(f"DRIFT: kimi config.toml missing the project hooks block "
561
- f"(run `mise run hooks-sync`)")
561
+ f"(run `mise run hooks:sync`)")
562
562
  rc = 1
563
563
  else:
564
564
  log(f"kimi: in sync ({kt})")
@@ -574,7 +574,7 @@ def cmd_check(master: dict) -> int:
574
574
  missing = [c for _ev, c, _t in hermes_commands(master) if c not in present]
575
575
  if missing:
576
576
  warn(f"DRIFT: hermes config.yaml missing {len(missing)} adapter hook(s) "
577
- f"(run `mise run hooks-sync`)")
577
+ f"(run `mise run hooks:sync`)")
578
578
  rc = 1
579
579
  else:
580
580
  log(f"hermes: in sync ({hcfg.relative_to(REPO_ROOT)})")
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "PER-DEV LOCAL OVERRIDES — copy this file to .agents/local.json (gitignored) and edit. Lets you opt out of individual hooks or whole injected agents without touching committed config. Hooks self-skip at RUNTIME via .agents/hooks/lib/hook-guard.sh (so even Claude's committed hooks honor it); codex/hermes injections additionally skip at INSTALL time. Re-run `mise run hooks-sync` (or re-cd into the repo) after editing.",
2
+ "$comment": "PER-DEV LOCAL OVERRIDES — copy this file to .agents/local.json (gitignored) and edit. Lets you opt out of individual hooks or whole injected agents without touching committed config. Hooks self-skip at RUNTIME via .agents/hooks/lib/hook-guard.sh (so even Claude's committed hooks honor it); codex/hermes injections additionally skip at INSTALL time. Re-run `mise run hooks:sync` (or re-cd into the repo) after editing.",
3
3
  "hooks": {
4
4
  "$disabled_help": "Hook ids from .agents/hooks/hooks.master.json: skill-check-reminder, hindsight-recall, hindsight-retain, hindsight-session-end. Listed ids are skipped at runtime across ALL agents.",
5
5
  "disabled": [],
@@ -11,7 +11,7 @@
11
11
  # .env untouched. Never prints the secret.
12
12
  #
13
13
  # Override the 1Password reference if the item path differs:
14
- # HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight-setup
14
+ # HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight:setup
15
15
  set -euo pipefail
16
16
 
17
17
  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -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)
@@ -50,6 +50,7 @@ role:
50
50
  default: pm
51
51
  choices:
52
52
  "Project Manager (pm)": pm
53
+ "Company Director (director)": director
53
54
  "Developer (dev)": dev
54
55
  "Reviewer (review)": review
55
56
  "Company Reporter — daily cross-project rollup (reporter)": reporter
@@ -69,7 +70,7 @@ ticket_provider:
69
70
  agent_purpose:
70
71
  type: str
71
72
  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 %}"
73
+ 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
74
 
74
75
  model_provider:
75
76
  type: str
@@ -81,6 +82,28 @@ model_name:
81
82
  help: "Model name (empty = inherit from global ~/.hermes config)"
82
83
  default: ""
83
84
 
85
+ model_base_url:
86
+ type: str
87
+ help: "Per-agent model API base URL (empty = inherit from shared config)"
88
+ default: ""
89
+
90
+ model_api_mode:
91
+ type: str
92
+ help: "Per-agent model API protocol (empty = inherit from shared config)"
93
+ default: ""
94
+ choices:
95
+ "Inherit from shared config": ""
96
+ "OpenAI-compatible chat completions": chat_completions
97
+ "OpenAI Codex responses": codex_responses
98
+ "Anthropic messages": anthropic_messages
99
+ "AWS Bedrock converse": bedrock_converse
100
+ "Codex app server": codex_app_server
101
+
102
+ model_key_env:
103
+ type: str
104
+ help: "Environment variable NAME holding the per-agent model key (never the key value)"
105
+ default: ""
106
+
84
107
  soul_tone:
85
108
  type: str
86
109
  help: "Tone for the agent's SOUL.md"
@@ -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
 
@@ -21,11 +21,11 @@ else
21
21
  cat > "$CONFIG" <<'TOML'
22
22
  # hermes-agent-template config — edit for your environment.
23
23
  [fleet]
24
- hermes_bin = "~/.hermes/hermes-agent/.venv/bin/hermes"
25
- hermes_repo = "~/.hermes/hermes-agent"
24
+ hermes_bin = "~/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1/.venv/bin/hermes"
25
+ hermes_repo = "~/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1"
26
26
  hermes_git_url = "https://github.com/delorenj/hermes-agent.git"
27
- hermes_git_ref = "feature/PJAN-19-routing-publication"
28
- hermes_git_sha = "113e1b182b6d72a7dd02a191f134a41668ceaf0e"
27
+ hermes_git_ref = "main"
28
+ hermes_git_sha = "0408fec7a153e6c32c064acd2b8053917f1525f1"
29
29
  fleet_env = "~/.hermes/fleet.env"
30
30
  registry_file = "~/.hermes/agents-registry.yaml"
31
31
  canonical_skills_dir = "/home/delorenj/.agents/skills"
@@ -1,10 +1,8 @@
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.
8
6
  # shellcheck source=_lib.sh
9
7
  source "$(dirname "$0")/_lib.sh"
10
8
  load_role_env
@@ -14,9 +12,6 @@ already_done 10-hermes-profile && { log "[10] profile already created — skippi
14
12
  log "[10] creating hermes profile: $PROFILE_NAME"
15
13
  PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
16
14
 
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
15
  if [[ -d "$PROFILE_HOME" ]]; then
21
16
  log " profile dir already exists; reusing"
22
17
  else
@@ -112,9 +107,9 @@ PYEOF
112
107
  fi
113
108
  fi
114
109
 
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"
110
+ # Never persist a project-specific terminal.cwd through the named profile.
111
+ # PJangler intentionally links profile config.yaml to the fleet-shared config;
112
+ # the generated launchers therefore pass TERMINAL_CWD process-locally instead.
118
113
 
119
114
  # Canonical shared-skill source of truth + local PM fallback sync.
120
115
  CANONICAL_SKILLS_DIR="${CANONICAL_SKILLS_DIR:-$(config_get fleet.canonical_skills_dir '/home/delorenj/.agents/skills')}"
@@ -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
@@ -46,15 +46,28 @@ path.write_text(text[: match.start("body")] + body + text[match.end("body") :],
46
46
  PYEOF
47
47
  }
48
48
 
49
- already_done 30-telegram && { log "[30] telegram already wired — skipping"; exit 0; }
49
+ telegram_status="$(yaml_get telegram.provisioning_status)"
50
50
 
51
51
  if [[ "${SKIP_TELEGRAM:-0}" == "1" ]]; then
52
- telegram_yaml_update provisioning_status disabled
53
- log "[30] telegram — SKIPPED (SKIP_TELEGRAM=1)"
54
- mark_done 30-telegram
52
+ if [[ "$telegram_status" == "verified" ]] && already_done 30-telegram; then
53
+ log "[30] telegram — SKIPPED; existing verified wiring preserved"
54
+ else
55
+ telegram_yaml_update provisioning_status disabled
56
+ clear_done 30-telegram
57
+ log "[30] telegram — DEFERRED (SKIP_TELEGRAM=1; completion marker cleared)"
58
+ fi
55
59
  exit 0
56
60
  fi
57
61
 
62
+ if already_done 30-telegram; then
63
+ if [[ "$telegram_status" == "verified" ]]; then
64
+ log "[30] telegram already wired — skipping"
65
+ exit 0
66
+ fi
67
+ clear_done 30-telegram
68
+ log "[30] stale deferred completion marker cleared — reconciling Telegram"
69
+ fi
70
+
58
71
  RUNTIME="$ROLE_DIR/runtime"
59
72
  ENVF="$RUNTIME/.env"
60
73
  mkdir -p "$RUNTIME"
@@ -81,6 +94,7 @@ fi
81
94
 
82
95
  if [[ -z "${TELEGRAM_BOT_TOKEN:-}" ]]; then
83
96
  telegram_yaml_update provisioning_status deferred
97
+ clear_done 30-telegram
84
98
  warn " no token provided; Telegram step deferred"
85
99
  warn " re-run later with a profile-dedicated TELEGRAM_BOT_TOKEN invocation"
86
100
  exit 0
@@ -12,9 +12,35 @@ if [[ "$ROLE" == "reporter" ]]; then
12
12
  exit 0
13
13
  fi
14
14
 
15
+ # Resolve expected paths without creating directories or querying the user
16
+ # manager. A legacy skip-created marker is complete only when all units exist.
15
17
  RUNTIME="$ROLE_DIR/runtime"
18
+ FLEET_HOME="${HERMES_FLEET_HOME:-$HOME/.hermes}"
19
+ PROFILE_HOME="$FLEET_HOME/profiles/${PROFILE_NAME:-$AGENT_ID}"
16
20
  REPO_ROOT="$(project_repo_path)" || REPO_ROOT="$ROLE_DIR"
17
21
  SYS_DIR="$HOME/.config/systemd/user"
22
+ GW_UNIT="hermes-${AGENT_ID}-gateway.service"
23
+ HB_SVC="hermes-${AGENT_ID}-heartbeat.service"
24
+ HB_TIMER="hermes-${AGENT_ID}-heartbeat.timer"
25
+
26
+ # Local-only provisioning must not inspect or mutate the host user manager.
27
+ # Honor the explicit skip before creating unit directories or querying legacy
28
+ # unit state; cleanup remains fail-closed whenever systemd management is active.
29
+ if [[ "${SKIP_SYSTEMD:-0}" == "1" ]]; then
30
+ if already_done 70-systemd \
31
+ && [[ -f "$SYS_DIR/$GW_UNIT" && -f "$SYS_DIR/$HB_SVC" && -f "$SYS_DIR/$HB_TIMER" ]]; then
32
+ log "[70] systemd — SKIPPED; existing complete unit set preserved"
33
+ else
34
+ clear_done 70-systemd
35
+ log "[70] systemd — DEFERRED (completion marker cleared)"
36
+ fi
37
+ exit 0
38
+ fi
39
+
40
+ # Singleton-runtime contract: units set HERMES_HOME to the agent's NAMED PROFILE
41
+ # dir, never the raw runtime path — Hermes derives profile identity and shared
42
+ # fleet auth from the unresolved HERMES_HOME. $RUNTIME stays correct for
43
+ # EnvironmentFile and logs: those are the repo-owned side the profile links to.
18
44
  mkdir -p "$SYS_DIR" "$RUNTIME/logs"
19
45
 
20
46
  # Upgrade remediation must run before honoring a legacy done marker. Older
@@ -56,17 +82,49 @@ if [[ $legacy_consumer_present -eq 1 ]]; then
56
82
  log " retired legacy per-profile Bloodbank consumer: $LEGACY_CONSUMER_UNIT"
57
83
  fi
58
84
 
59
- already_done 70-systemd && { log "[70] systemd already installed — legacy cleanup checked"; exit 0; }
60
- [[ "${SKIP_SYSTEMD:-0}" == "1" ]] && { log "[70] systemd — SKIPPED"; mark_done 70-systemd; exit 0; }
85
+ if already_done 70-systemd; then
86
+ if [[ -f "$SYS_DIR/$GW_UNIT" && -f "$SYS_DIR/$HB_SVC" && -f "$SYS_DIR/$HB_TIMER" ]]; then
87
+ log "[70] systemd already installed — reconciling unit definitions"
88
+ else
89
+ clear_done 70-systemd
90
+ log " stale/incomplete systemd marker cleared — reconciling required units"
91
+ fi
92
+ fi
61
93
 
62
94
  # The heartbeat runner (board-reconciliation sentinel pass + gated checkpoint)
63
95
  # and the checkpoint helper both render into the role dir; just ensure they are
64
96
  # executable. heartbeat.sh calls checkpoint.sh internally.
65
97
  HEARTBEAT_BIN="$ROLE_DIR/.scripts/heartbeat.sh"
66
- chmod +x "$HEARTBEAT_BIN" "$ROLE_DIR/.scripts/checkpoint.sh" 2>/dev/null || true
98
+ CREDENTIAL_LAUNCHER="$ROLE_DIR/.scripts/credential-launch.sh"
99
+ chmod +x "$HEARTBEAT_BIN" "$CREDENTIAL_LAUNCHER" "$ROLE_DIR/.scripts/checkpoint.sh" 2>/dev/null || true
100
+
101
+ [[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]] \
102
+ || die "named profile is not a real directory; run: pj migrate hermes.runtime-singleton '$REPO_ROOT'"
103
+
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
67
126
 
68
127
  # Gateway unit
69
- GW_UNIT="hermes-${AGENT_ID}-gateway.service"
70
128
  cat > "$SYS_DIR/$GW_UNIT" <<UNIT
71
129
  [Unit]
72
130
  Description=Hermes Gateway — $DISPLAY_NAME
@@ -75,11 +133,13 @@ Wants=network-online.target
75
133
 
76
134
  [Service]
77
135
  Type=simple
78
- Environment=HERMES_HOME=$RUNTIME
79
- Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
136
+ Environment=HERMES_HOME=$PROFILE_HOME
137
+ Environment=HERMES_BIN=$HERMES_BIN
80
138
  Environment=CODEX_HOME=$CODEX_HOME
139
+ Environment="TERMINAL_CWD=$REPO_ROOT"
81
140
  EnvironmentFile=-$RUNTIME/.env
82
- ExecStart=$HERMES_BIN gateway run --replace
141
+ $GW_CREDENTIAL_LINES
142
+ ExecStart=$CREDENTIAL_LAUNCHER gateway
83
143
  Restart=on-failure
84
144
  RestartSec=10
85
145
  StandardOutput=append:$RUNTIME/logs/gateway.systemd.log
@@ -93,8 +153,6 @@ UNIT
93
153
  # Frequent ticks (1 min); heartbeat.sh's own cooldown/lock logic rate-limits the
94
154
  # full Hermes pass, and the checkpoint is gated to ~hourly inside the runner.
95
155
  # The per-agent EnvironmentFiles load ticket-provider creds for the sentinel pass.
96
- HB_SVC="hermes-${AGENT_ID}-heartbeat.service"
97
- HB_TIMER="hermes-${AGENT_ID}-heartbeat.timer"
98
156
  cat > "$SYS_DIR/$HB_SVC" <<UNIT
99
157
  [Unit]
100
158
  Description=Hermes Heartbeat (reconcile + checkpoint) — $DISPLAY_NAME
@@ -104,15 +162,17 @@ Wants=network-online.target
104
162
  [Service]
105
163
  Type=oneshot
106
164
  WorkingDirectory=$REPO_ROOT
107
- Environment=HERMES_HOME=$RUNTIME
108
- Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
165
+ Environment=HERMES_HOME=$PROFILE_HOME
166
+ Environment=HERMES_BIN=$HERMES_BIN
109
167
  Environment=CODEX_HOME=$CODEX_HOME
168
+ Environment="TERMINAL_CWD=$REPO_ROOT"
110
169
  EnvironmentFile=-%h/.config/hermes-agent/env
111
170
  EnvironmentFile=-%h/.hermes/env
112
171
  EnvironmentFile=-%h/.hermes/hermes-agent.env
113
172
  EnvironmentFile=-%h/.hermes/${AGENT_ID}.env
114
173
  EnvironmentFile=-$RUNTIME/.env
115
- ExecStart=$HEARTBEAT_BIN
174
+ $HB_CREDENTIAL_LINES
175
+ ExecStart=$CREDENTIAL_LAUNCHER heartbeat
116
176
  TimeoutStartSec=45min
117
177
  StandardOutput=append:$RUNTIME/logs/heartbeat.log
118
178
  StandardError=append:$RUNTIME/logs/heartbeat.log