@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.
- package/dist/index.js +295 -130
- package/dist/mcp-server.js +297 -129
- package/package.json +1 -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.toml.jinja +11 -11
- package/templates/hermes-agent/copier.yml +27 -4
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
- package/templates/hermes-agent/template/.scripts/01-config.sh +4 -4
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +7 -12
- 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/70-systemd.sh +72 -12
- package/templates/hermes-agent/template/.scripts/80-registry.sh +13 -3
- package/templates/hermes-agent/template/.scripts/_lib.sh +12 -5
- 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 +2 -1
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +30 -4
- package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
- package/templates/hermes-agent/template/SOUL.md.jinja +4 -4
- package/templates/hermes-agent/template/hermes.jinja +36 -7
- package/templates/hermes-agent/template/role.yaml.jinja +15 -5
package/package.json
CHANGED
|
@@ -12,11 +12,11 @@ master writes zero bytes).
|
|
|
12
12
|
## TL;DR
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
mise run hooks
|
|
16
|
-
mise run hooks
|
|
17
|
-
mise run hooks
|
|
18
|
-
mise run skills
|
|
19
|
-
mise run hindsight
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
@@ -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
|
|
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
|
|
|
@@ -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 = "~/.
|
|
25
|
-
hermes_repo = "~/.
|
|
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 = "
|
|
28
|
-
hermes_git_sha = "
|
|
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
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
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
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
@@ -46,15 +46,28 @@ path.write_text(text[: match.start("body")] + body + text[match.end("body") :],
|
|
|
46
46
|
PYEOF
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
telegram_status="$(yaml_get telegram.provisioning_status)"
|
|
50
50
|
|
|
51
51
|
if [[ "${SKIP_TELEGRAM:-0}" == "1" ]]; then
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
60
|
-
[[ "$
|
|
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
|
-
|
|
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=$
|
|
79
|
-
Environment=
|
|
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
|
-
|
|
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=$
|
|
108
|
-
Environment=
|
|
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
|
-
|
|
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
|