@delorenj/pjangler 1.4.5 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +41 -520
  2. package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
  3. package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
  4. package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
  5. package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
  6. package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
  7. package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
  8. package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
  9. package/dist/index.js +5747 -13483
  10. package/dist/mcp-server.js +6853 -14059
  11. package/dist/notebook/journal-service-main.js +5682 -0
  12. package/dist/project-registry-service.js +748 -0
  13. package/dist/prompt.js +2 -1
  14. package/package.json +15 -10
  15. package/scripts/install-project-registry.mjs +34 -0
  16. package/scripts/migrate-project-registry.mjs +117 -0
  17. package/src/project/registryClient.ts +62 -0
  18. package/templates/commonproject/copier.yml +16 -19
  19. package/templates/commonproject/template/.agents/hooks/README.md +10 -4
  20. package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
  21. package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
  22. package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
  23. package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
  24. package/templates/commonproject/template/.agents/skills.json +0 -1
  25. package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
  26. package/templates/commonproject/template/.project.json.jinja +1 -1
  27. package/templates/commonproject/template/mise.toml.jinja +11 -21
  28. package/contracts/fleet-contract.yaml +0 -513
  29. package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
  30. package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
  31. package/templates/hermes-agent/copier.yml +0 -186
  32. package/templates/hermes-agent/template/.gitignore.jinja +0 -11
  33. package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
  34. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
  35. package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
  36. package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
  37. package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
  38. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
  39. package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
  40. package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
  41. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
  42. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
  43. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
  44. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
  45. package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
  46. package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
  47. package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
  48. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
  49. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
  50. package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
  51. package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
  52. package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
  53. package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
  54. package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
  55. package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
  56. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
  57. package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
  58. package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
  59. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
  60. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
  61. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
  62. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
  63. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
  64. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
  65. package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
  66. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
  67. package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
  68. package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
  69. package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
  70. package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
  71. package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
  72. package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
  73. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
  74. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
  75. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
  76. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
  77. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
  78. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
  79. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
  80. package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
  81. package/templates/hermes-agent/template/hermes.jinja +0 -109
  82. package/templates/hermes-agent/template/momo.jinja +0 -177
  83. package/templates/hermes-agent/template/role.yaml.jinja +0 -112
@@ -1,222 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Create or reconcile the initial named Hermes profile without cloning secrets.
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
-
12
- # shellcheck source=_lib.sh
13
- source "$(dirname "$0")/_lib.sh"
14
- load_role_env
15
-
16
- PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
17
-
18
- # Older deployments made the named profile itself a symlink into project
19
- # state. Following that link here would make the cleanup below delete files
20
- # from the link target before the singleton-runtime migration can preserve
21
- # them. Refuse the legacy topology before any profile mutation.
22
- if [[ -L "$PROFILE_HOME" ]]; then
23
- die "legacy named profile symlink detected at $PROFILE_HOME; refusing mutation. Run: pj migrate hermes.runtime-singleton '$(project_repo_path 2>/dev/null || printf '%s' "$ROLE_DIR")'"
24
- fi
25
- PROFILE_DELTA_SEEDER="$ROLE_DIR/.scripts/lib/profile-config-seed.py"
26
- [[ -f "$PROFILE_DELTA_SEEDER" && ! -L "$PROFILE_DELTA_SEEDER" ]] \
27
- || die "trusted profile config seed helper is unavailable: $PROFILE_DELTA_SEEDER"
28
-
29
- already_done 10-hermes-profile \
30
- && log "[10] profile marker found — revalidating required profile contract"
31
-
32
- # Skillex projects the canonical global catalog into ~/.agents/skills. These
33
- # six skills are the immutable deployed PM contract, not optional suggestions.
34
- # Validate the complete set before any profile mutation so a partial/missing
35
- # projection cannot warn and then be falsely reported as provisioned.
36
- CANONICAL_SKILLS_DIR="${CANONICAL_SKILLS_DIR:-$(config_get fleet.canonical_skills_dir "$HOME/.agents/skills")}"
37
- CORE_RUNTIME_SKILLS=(
38
- 33god-projects
39
- delonet-conventions
40
- delonet-dotenv
41
- hermes-pm-template-maintenance
42
- hindsight
43
- subagent-driven-development
44
- )
45
- OPTIONAL_RUNTIME_SKILLS_TEXT="${SYMLINKED_RUNTIME_SKILLS:-$(config_get fleet.symlinked_runtime_skills '')}"
46
- read -r -a OPTIONAL_RUNTIME_SKILLS <<< "$OPTIONAL_RUNTIME_SKILLS_TEXT"
47
- SYMLINKED_RUNTIME_SKILLS=("${CORE_RUNTIME_SKILLS[@]}")
48
- for skill_name in "${OPTIONAL_RUNTIME_SKILLS[@]}"; do
49
- [[ -n "$skill_name" ]] || continue
50
- skill_present=0
51
- for required_name in "${SYMLINKED_RUNTIME_SKILLS[@]}"; do
52
- [[ "$required_name" == "$skill_name" ]] && { skill_present=1; break; }
53
- done
54
- [[ $skill_present -eq 1 ]] || SYMLINKED_RUNTIME_SKILLS+=("$skill_name")
55
- done
56
- missing_runtime_skills=()
57
- for skill_name in "${SYMLINKED_RUNTIME_SKILLS[@]}"; do
58
- [[ -f "$CANONICAL_SKILLS_DIR/$skill_name/SKILL.md" ]] \
59
- || missing_runtime_skills+=("$skill_name")
60
- done
61
- if [[ ${#missing_runtime_skills[@]} -gt 0 ]]; then
62
- clear_done 10-hermes-profile
63
- die "required Skillex projection missing SKILL.md for: ${missing_runtime_skills[*]} (run the global Skillex sync, then rerun)"
64
- fi
65
- log "[10] required Skillex skills validated: ${SYMLINKED_RUNTIME_SKILLS[*]}"
66
-
67
- log "[10] creating hermes profile: $PROFILE_NAME"
68
-
69
- if [[ -d "$PROFILE_HOME" ]]; then
70
- log " profile dir already exists; reusing"
71
- else
72
- # `--clone` copies the default profile's .env before a provisioner can
73
- # inspect it, transiently materializing every credential in the new profile.
74
- # Start clean; required skills and the project SOUL are installed below.
75
- "$HERMES_BIN" profile create "$PROFILE_NAME" --no-alias
76
- fi
77
-
78
- # Strip any inherited gateway/runtime state so this profile boots clean.
79
- rm -f "$PROFILE_HOME/gateway.pid" "$PROFILE_HOME/gateway_state.json" \
80
- "$PROFILE_HOME/processes.json" "$PROFILE_HOME/state.db" 2>/dev/null || true
81
- # Belt-and-suspenders: if a profiles/ dir somehow exists, remove it
82
- [[ -d "$PROFILE_HOME/profiles" ]] && rm -rf "$PROFILE_HOME/profiles"
83
-
84
- # A new profile gets an empty Hermes-created .env. Existing profiles are never
85
- # migrated opportunistically: if a legacy deployment still has raw channel
86
- # credentials, fail closed and leave the approval-gated migration to the fleet
87
- # operator. Only key names are reported; values are never printed.
88
- PROFILE_ENV="$PROFILE_HOME/.env"
89
- if [[ -f "$PROFILE_ENV" ]]; then
90
- python3 - "$PROFILE_ENV" <<'PYEOF'
91
- import sys, pathlib
92
- p = pathlib.Path(sys.argv[1])
93
- keys = (
94
- "SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_SIGNING_SECRET",
95
- "TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN",
96
- )
97
- found = []
98
- for raw in p.read_text(encoding="utf-8").splitlines():
99
- line = raw.strip()
100
- if not line or line.startswith("#"):
101
- continue
102
- if line.startswith("export "):
103
- line = line[7:].lstrip()
104
- name, sep, value = line.partition("=")
105
- if sep and name.strip() in keys and value.strip():
106
- found.append(name.strip())
107
- if found:
108
- raise SystemExit(
109
- "raw channel credential assignments require the approval-gated "
110
- "1Password migration: " + ", ".join(sorted(set(found)))
111
- )
112
- PYEOF
113
- [[ -L "$PROFILE_ENV" ]] || chmod 600 "$PROFILE_ENV"
114
- fi
115
-
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
- profile_delta_seed_result="$(
131
- python3 -I "$PROFILE_DELTA_SEEDER" --profile "$PROFILE_HOME"
132
- )" || die "config.delta.yaml seed reconciliation failed"
133
- if [[ "$profile_delta_seed_result" == "seeded" ]]; then
134
- log " seeding empty config.delta.yaml (override-only SSOT)"
135
- elif [[ "$profile_delta_seed_result" != "exists" ]]; then
136
- die "profile config seed helper returned an invalid result"
137
- fi
138
-
139
- # Pin the identity-memory bank explicitly rather than relying on the fleet
140
- # bank_id_template (agent-{profile}). {profile} resolves through Hermes'
141
- # get_active_profile_name(), which calls Path.resolve() on HERMES_HOME and
142
- # requires a lowercase id sitting directly under profiles/. A symlinked profile
143
- # dir or an uppercase name silently yields the literal "custom" — which would
144
- # merge this agent's PRIVATE memory into a bank shared with every other agent
145
- # that also failed to resolve.
146
- PROFILE_MEM_CFG="$PROFILE_HOME/hindsight/config.json"
147
- mkdir -p "$(dirname "$PROFILE_MEM_CFG")"
148
- if [[ ! -f "$PROFILE_MEM_CFG" ]]; then
149
- log " pinning identity-memory bank: agent-$PROFILE_NAME"
150
- printf '{\n "bank_id": "agent-%s"\n}\n' "$PROFILE_NAME" > "$PROFILE_MEM_CFG"
151
- chmod 600 "$PROFILE_MEM_CFG"
152
- fi
153
-
154
- # Render config.yaml from base + delta when the renderer is available. Without
155
- # it the profile still boots (Hermes reads whatever config.yaml exists), but it
156
- # is not yet under inheritance and `pj audit` will say so.
157
- PROFILE_RENDERER="${PROFILE_RENDERER:-$HOME/code/33GOD/hermes-agent-template/scripts/hermes-profile-config.py}"
158
- if [[ -x "$PROFILE_RENDERER" || -f "$PROFILE_RENDERER" ]]; then
159
- log " rendering config.yaml from fleet base + delta"
160
- python3 "$PROFILE_RENDERER" render --profile "$PROFILE_NAME" >/dev/null 2>&1 \
161
- || warn " render failed; run hermes-profile-config.py render --profile $PROFILE_NAME"
162
- else
163
- warn " profile renderer not found at $PROFILE_RENDERER — config.yaml not rendered"
164
- fi
165
-
166
- # Canonical shared-skill source of truth + local PM fallback sync.
167
- CANONICAL_PM_SKILL_SRC="$CANONICAL_SKILLS_DIR/subagent-driven-development"
168
- LOCAL_PM_SKILL_DST="$PROFILE_HOME/skills/software-development/subagent-driven-development"
169
-
170
- if [[ -d "$CANONICAL_SKILLS_DIR" ]]; then
171
- # skills.external_dirs is a FLEET setting and already lives in
172
- # ~/.hermes/config.yaml, so every rendered profile inherits it. Writing it
173
- # per-profile here would (a) be redundant, and (b) write into the GENERATED
174
- # config.yaml, where the next render discards it — the classic "I set it and
175
- # it reverted" trap. Verify inheritance instead of re-asserting it.
176
- if ! env HERMES_HOME="$PROFILE_HOME" "$HERMES_BIN" config get skills.external_dirs 2>/dev/null \
177
- | grep -qF "$CANONICAL_SKILLS_DIR"; then
178
- warn " skills.external_dirs does not include $CANONICAL_SKILLS_DIR"
179
- warn " add it to the FLEET base (~/.hermes/config.yaml), then: hermes-profile-config.py render --all"
180
- else
181
- log " skills.external_dirs inherited from fleet base: $CANONICAL_SKILLS_DIR"
182
- fi
183
-
184
- # Ensure key PM/local-ops skills are symlinked into runtime/profile skills root.
185
- # This preserves canonical ownership and keeps updates instant across agents.
186
- mkdir -p "$PROFILE_HOME/skills"
187
-
188
- for skill_name in "${SYMLINKED_RUNTIME_SKILLS[@]}"; do
189
- src="$CANONICAL_SKILLS_DIR/$skill_name"
190
- dst="$PROFILE_HOME/skills/$skill_name"
191
-
192
- [[ -f "$src/SKILL.md" ]] \
193
- || die "required Skillex skill disappeared during provisioning: $src/SKILL.md"
194
-
195
- if [[ -L "$dst" && "$(readlink "$dst")" == "$src" ]]; then
196
- log " runtime skill symlink already set: $dst -> $src"
197
- continue
198
- fi
199
-
200
- [[ -e "$dst" || -L "$dst" ]] && rm -rf "$dst"
201
- ln -s "$src" "$dst"
202
- log " symlinked runtime skill: $dst -> $src"
203
- done
204
- else
205
- die "canonical Skillex projection directory disappeared during provisioning: $CANONICAL_SKILLS_DIR"
206
- fi
207
-
208
- if [[ -f "$CANONICAL_PM_SKILL_SRC/SKILL.md" ]]; then
209
- log " syncing canonical PM workflow skill -> $LOCAL_PM_SKILL_DST"
210
- mkdir -p "$LOCAL_PM_SKILL_DST"
211
- cp -f "$CANONICAL_PM_SKILL_SRC/SKILL.md" "$LOCAL_PM_SKILL_DST/SKILL.md"
212
- else
213
- die "required canonical PM skill disappeared during provisioning: $CANONICAL_PM_SKILL_SRC/SKILL.md"
214
- fi
215
-
216
- # Install the project's SOUL.md into the profile so the agent loads it.
217
- if [[ -f "$ROLE_DIR/SOUL.md" ]]; then
218
- cp "$ROLE_DIR/SOUL.md" "$PROFILE_HOME/SOUL.md"
219
- log " installed SOUL.md into profile"
220
- fi
221
-
222
- mark_done 10-hermes-profile
@@ -1,199 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Provision the per-agent, pure-local Hermes runtime without creating a Git
3
- # repository or a project submodule. Runtime durability belongs to Hindsight;
4
- # this directory may contain secrets and mutable agent state (PJAN-41).
5
- # shellcheck source=_lib.sh
6
- source "$(dirname "$0")/_lib.sh"
7
- load_role_env
8
-
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
16
-
17
- PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
18
- RUNTIME_LOCAL="$ROLE_DIR/runtime"
19
- PROJECT_PATH="$(project_repo_path)" || die "no project git root"
20
- REL_ROLE_PATH="$(realpath --relative-to="$PROJECT_PATH" "$ROLE_DIR")"
21
- REL_RUNTIME_PATH="${REL_ROLE_PATH}/runtime"
22
-
23
- log "[20] local runtime: $RUNTIME_LOCAL"
24
-
25
- # Fail closed if an older installation still models runtime as a project
26
- # submodule. `pjangler migrate` performs the non-destructive index transition;
27
- # this provisioner never removes or rewrites an existing nested repository.
28
- if git -C "$PROJECT_PATH" ls-files --stage -- "$REL_RUNTIME_PATH" | grep -q '^160000 '; then
29
- die "$REL_RUNTIME_PATH is still a tracked gitlink; run 'pjangler migrate' before provisioning"
30
- fi
31
- if [[ -f "$PROJECT_PATH/.gitmodules" ]] &&
32
- git -C "$PROJECT_PATH" config -f .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null |
33
- awk -v expected="$REL_RUNTIME_PATH" '$2 == expected { found=1 } END { exit !found }'; then
34
- die "$REL_RUNTIME_PATH still has a stale .gitmodules mapping; run 'pjangler migrate' before provisioning"
35
- fi
36
-
37
- mkdir -p "$RUNTIME_LOCAL"
38
- if [[ -e "$RUNTIME_LOCAL/.git" ]]; then
39
- warn " existing nested runtime repository preserved; no fetch, commit, or push will be attempted"
40
- fi
41
-
42
- # Render the scaffold in a temporary directory, then copy only missing paths.
43
- # Existing memory, configuration, credentials, and sessions always win.
44
- TMP="$(mktemp -d)"
45
- cleanup() { rm -rf -- "$TMP"; }
46
- trap cleanup EXIT
47
- cp -a "$RUNTIME_SCAFFOLD_DIR/." "$TMP/"
48
- python3 - "$TMP" "$AGENT_ID" "$REPO" "$ROLE" "$DISPLAY_NAME" <<'PYEOF'
49
- import pathlib
50
- import sys
51
-
52
- root, agent_id, repo, role, display = sys.argv[1:6]
53
- root = pathlib.Path(root)
54
- mapping = {
55
- "{{agent_id}}": agent_id,
56
- "{{repo}}": repo,
57
- "{{role}}": role,
58
- "{{display_name}}": display,
59
- }
60
- for path in root.rglob("*"):
61
- if path.is_file() and path.suffix in (".md", ".yaml", ".yml", ".sh", ".py", ".gitignore", ".gitattributes"):
62
- try:
63
- text = path.read_text()
64
- for source, target in mapping.items():
65
- text = text.replace(source, target)
66
- path.write_text(text)
67
- except UnicodeDecodeError:
68
- pass
69
- PYEOF
70
- # Never let a literal secret reach the runtime. The scaffold is rendered from
71
- # templates, so a leaked credential shows up here before anything is copied.
72
- python3 "$(dirname "$0")/secret-scan.py" "$TMP"
73
- python3 - "$TMP" "$RUNTIME_LOCAL" <<'PYEOF'
74
- import os, pathlib, shutil, sys
75
-
76
- source = pathlib.Path(sys.argv[1])
77
- target = pathlib.Path(sys.argv[2])
78
- for current in sorted(source.rglob("*")):
79
- relative = current.relative_to(source)
80
- destination = target / relative
81
- if current.is_dir():
82
- destination.mkdir(parents=True, exist_ok=True)
83
- continue
84
- if os.path.lexists(destination):
85
- continue
86
- destination.parent.mkdir(parents=True, exist_ok=True)
87
- if current.is_symlink():
88
- destination.symlink_to(os.readlink(current))
89
- else:
90
- shutil.copy2(current, destination, follow_symlinks=False)
91
- PYEOF
92
-
93
- # Seed mutable identity/config only when no local value exists.
94
- if [[ "$ROLE" == "reporter" ]]; then
95
- # Generate a delta-only runtime config for least-privilege reporters. Never copy the shared PM
96
- # config: it may carry dashboard credentials, write-capable MCPs, or broad tools.
97
- MODEL_PROVIDER="$(yaml_get model.provider)"
98
- MODEL_NAME="$(yaml_get model.name)"
99
- CANONICAL_SKILLS_DIR="${CANONICAL_SKILLS_DIR:-$(config_get fleet.canonical_skills_dir "$HOME/.agents/skills")}"
100
- if [[ ! -e "$RUNTIME_LOCAL/config.yaml" ]]; then
101
- python3 - "$RUNTIME_LOCAL/config.yaml" "$PROJECT_PATH" "${HERMES_TIMEZONE:-America/New_York}" \
102
- "$MODEL_PROVIDER" "$MODEL_NAME" "$CANONICAL_SKILLS_DIR" "$ROLE" <<'PYEOF'
103
- import json, pathlib, re, sys
104
- path, cwd, timezone, provider, model, skills, role = sys.argv[1:8]
105
- if not re.fullmatch(r"[A-Za-z_]+(?:/[A-Za-z_]+)*", timezone):
106
- raise SystemExit("unsafe timezone")
107
- config = {
108
- "timezone": timezone,
109
- "terminal": {"cwd": cwd},
110
- "skills": {"external_dirs": [skills]},
111
- }
112
- if provider or model:
113
- config["model"] = {}
114
- if provider:
115
- config["model"]["provider"] = provider
116
- if model:
117
- config["model"]["default"] = model
118
- config["platform_toolsets"] = {
119
- "cli": ["web", "delegation", "no_mcp"],
120
- "cron": ["web", "delegation", "no_mcp"],
121
- }
122
- config["agent"] = {
123
- "disabled_toolsets": [
124
- "browser", "terminal", "file", "code_execution", "cronjob",
125
- "kanban", "homeassistant", "computer_use", "project", "skills",
126
- ]
127
- }
128
- config["delegation"] = {"max_spawn_depth": 1, "inherit_mcp_toolsets": False}
129
- pathlib.Path(path).write_text(json.dumps(config, indent=2) + "\n")
130
- PYEOF
131
- fi
132
- if [[ ! -e "$RUNTIME_LOCAL/profile.yaml" ]]; then
133
- cat > "$RUNTIME_LOCAL/profile.yaml" <<'YAML'
134
- config:
135
- inherit_from: default
136
- save_mode: delta
137
- YAML
138
- fi
139
- else
140
- CANONICAL_PM_CONFIG="$(config_get fleet.canonical_pm_config "$HOME/.hermes/config.yaml")"
141
- if [[ -f "$CANONICAL_PM_CONFIG" && ! -e "$RUNTIME_LOCAL/config.yaml" ]]; then
142
- cp "$CANONICAL_PM_CONFIG" "$RUNTIME_LOCAL/config.yaml"
143
- fi
144
- fi
145
- if [[ ! -e "$RUNTIME_LOCAL/SOUL.md" ]]; then
146
- cp "$ROLE_DIR/SOUL.md" "$RUNTIME_LOCAL/SOUL.md"
147
- fi
148
-
149
- # Named-profile topology belongs exclusively to PJangler. Preview first, then
150
- # apply the one idempotent rule. This script never removes or replaces a real
151
- # profile directory and never creates the obsolete profile -> runtime symlink.
152
- if [[ "$PJANGLER_BIN" != */* ]]; then
153
- PJANGLER_BIN="$(command -v "$PJANGLER_BIN" 2>/dev/null || true)"
154
- fi
155
- [[ -n "$PJANGLER_BIN" && -x "$PJANGLER_BIN" ]] \
156
- || die "PJángler CLI not found; install 'pj' or set PJANGLER_BIN"
157
- "$PJANGLER_BIN" migrate hermes.runtime-singleton "$PROJECT_PATH" --dry-run --json >/dev/null \
158
- || die "singleton-runtime audit failed; profile was left untouched"
159
- "$PJANGLER_BIN" migrate hermes.runtime-singleton "$PROJECT_PATH" --json >/dev/null \
160
- || die "singleton-runtime migration failed; inspect with: pj migrate hermes.runtime-singleton '$PROJECT_PATH' --dry-run"
161
- [[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]] \
162
- || die "PJángler did not establish a real named profile at $PROFILE_HOME"
163
- log " singleton profile verified by pj migrate hermes.runtime-singleton: $PROFILE_HOME"
164
-
165
- # Never persist a project-specific terminal.cwd through the named profile.
166
- # config.yaml is fleet-shared in the singleton topology. The manual and
167
- # service launchers pass TERMINAL_CWD process-locally for this role instead.
168
-
169
- if [[ "$ROLE" == "pm" ]]; then
170
- VOX_PLUGIN_NAME="${VOX_PLUGIN_NAME:-$(config_get fleet.vox_plugin_name 'vox')}"
171
- VOX_VOICE="${VOX_VOICE:-$(config_get fleet.vox_voice 'carlin')}"
172
- VOX_PLUGIN_DIR="${VOX_PLUGIN_DIR:-$(config_get fleet.vox_plugin_dir "$HOME/code/voxxy/plugins/tts/$VOX_PLUGIN_NAME")}"
173
- if [[ -d "$VOX_PLUGIN_DIR" ]]; then
174
- mkdir -p "$PROFILE_HOME/plugins/tts"
175
- ln -sfn "$VOX_PLUGIN_DIR" "$PROFILE_HOME/plugins/tts/$VOX_PLUGIN_NAME"
176
- profile_voice_contract_set "$PROFILE_HOME" "$VOX_PLUGIN_NAME" "$VOX_VOICE" \
177
- || die "could not write the canonical PM voice contract to config.delta.yaml"
178
- python3 - "$PROFILE_HOME/config.yaml" "$VOX_PLUGIN_NAME" "$VOX_VOICE" <<'PYEOF' \
179
- || die "PM voice config is not the canonical vox/carlin contract"
180
- import pathlib, sys
181
- try:
182
- import yaml
183
- except ImportError:
184
- raise SystemExit("PyYAML is required to validate PM voice config")
185
- config = yaml.safe_load(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) or {}
186
- plugin, voice = sys.argv[2:4]
187
- enabled = ((config.get("plugins") or {}).get("enabled") or [])
188
- tts = config.get("tts") or {}
189
- actual_voice = ((tts.get(plugin) or {}).get("voice") or tts.get("voice") or "")
190
- if f"tts/{plugin}" not in enabled or tts.get("provider") != plugin or actual_voice != voice:
191
- raise SystemExit(1)
192
- PYEOF
193
- log " PM voice verified: provider=$VOX_PLUGIN_NAME voice=$VOX_VOICE"
194
- else
195
- log " optional Vox plugin not installed; PM voice activation deferred"
196
- fi
197
- fi
198
-
199
- mark_done 20-runtime-repo