@delorenj/pjangler 1.4.6 → 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.
- package/README.md +41 -596
- package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
- package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
- package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
- package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
- package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
- package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
- package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
- package/dist/index.js +9385 -18379
- package/dist/mcp-server.js +9432 -17852
- package/dist/notebook/journal-service-main.js +5682 -0
- package/dist/project-registry-service.js +748 -0
- package/dist/prompt.js +2 -1
- package/package.json +15 -10
- package/scripts/install-project-registry.mjs +34 -0
- package/scripts/migrate-project-registry.mjs +117 -0
- package/src/project/registryClient.ts +62 -0
- package/templates/commonproject/copier.yml +16 -19
- package/templates/commonproject/template/.agents/hooks/README.md +10 -4
- package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
- package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
- package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
- package/templates/commonproject/template/.agents/skills.json +0 -1
- package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
- package/templates/commonproject/template/.project.json.jinja +1 -1
- package/templates/commonproject/template/mise.toml.jinja +11 -21
- package/contracts/fleet-contract.yaml +0 -584
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
- package/templates/hermes-agent/copier.yml +0 -186
- package/templates/hermes-agent/template/.gitignore.jinja +0 -11
- package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
- package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
- package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
- package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
- package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
- package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
- package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
- package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
- package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
- package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
- package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
- package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
- package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
- package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
- package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
- package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
- package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
- package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
- package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
- package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
- package/templates/hermes-agent/template/hermes.jinja +0 -109
- package/templates/hermes-agent/template/momo.jinja +0 -177
- 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
|