@delorenj/pjangler 1.4.2 → 1.4.4
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 +528 -0
- package/contracts/fleet-contract.yaml +513 -0
- package/dist/index.js +9333 -1509
- package/dist/mcp-server.js +6634 -1096
- package/dist/prompt.js +2 -1
- package/package.json +10 -4
- package/templates/hermes-agent/copier.yml +16 -3
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +7 -4
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +88 -94
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +44 -21
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +182 -171
- package/templates/hermes-agent/template/.scripts/31-slack.sh +260 -165
- package/templates/hermes-agent/template/.scripts/40-plane.sh +45 -36
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +210 -41
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +129 -15
- package/templates/hermes-agent/template/.scripts/80-registry.sh +42 -6
- package/templates/hermes-agent/template/.scripts/99-summary.sh +69 -16
- package/templates/hermes-agent/template/.scripts/_lib.sh +773 -0
- package/templates/hermes-agent/template/.scripts/channel-transaction.py +2340 -0
- package/templates/hermes-agent/template/.scripts/config.example.toml +8 -2
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +5 -1
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -3
- package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +182 -0
- package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +108 -0
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +93 -4
- package/templates/hermes-agent/template/.scripts/lib/voice-config.py +546 -0
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +138 -25
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +408 -51
- package/templates/hermes-agent/template/.scripts/providers/trello.sh +54 -6
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +257 -43
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +142 -25
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +13 -19
- package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +29 -36
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +3 -1
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -8
- package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +260 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +14 -16
- package/templates/hermes-agent/template/hermes.jinja +1 -1
- package/templates/hermes-agent/template/role.yaml.jinja +15 -4
package/dist/prompt.js
CHANGED
|
@@ -241,7 +241,7 @@ function planeBase(env = process.env, home = homedir()) {
|
|
|
241
241
|
if (fromConfig) return fromConfig.replace(/\/+$/, "");
|
|
242
242
|
return DEFAULT_PLANE_BASE;
|
|
243
243
|
}
|
|
244
|
-
function planeWorkspace(provider, env, home) {
|
|
244
|
+
function planeWorkspace(provider, env = process.env, home = homedir()) {
|
|
245
245
|
const fromManifest = provider.workspace?.trim();
|
|
246
246
|
if (fromManifest) return fromManifest;
|
|
247
247
|
const config = readTemplateConfig(env, home);
|
|
@@ -402,3 +402,4 @@ export {
|
|
|
402
402
|
promptLine,
|
|
403
403
|
readPromptFacts
|
|
404
404
|
};
|
|
405
|
+
//# sourceMappingURL=prompt.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delorenj/pjangler",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Project subsystem bootstrapper CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
|
+
"!dist/**/*.map",
|
|
16
|
+
"contracts",
|
|
15
17
|
"templates/commonproject/copier.yml",
|
|
16
18
|
"templates/commonproject/template",
|
|
17
19
|
"templates/hermes-agent/copier.yml",
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
"node": ">=20"
|
|
24
26
|
},
|
|
25
27
|
"scripts": {
|
|
26
|
-
"build": "esbuild src/index.ts src/mcp-server.ts src/prompt.ts --bundle --packages=external --platform=node --format=esm --outdir=dist && node scripts/export-project-notebook-skill.mjs",
|
|
28
|
+
"build": "esbuild src/index.ts src/mcp-server.ts src/prompt.ts --bundle --packages=external --platform=node --format=esm --sourcemap --outdir=dist && node scripts/export-project-notebook-skill.mjs",
|
|
27
29
|
"check:audit:prod": "npm audit --omit=dev",
|
|
28
30
|
"check:lock": "node scripts/check-package-lock-parity.mjs",
|
|
29
31
|
"check:submodules": "node scripts/check-submodule-contract.mjs",
|
|
@@ -33,12 +35,15 @@
|
|
|
33
35
|
"typecheck": "tsc --noEmit",
|
|
34
36
|
"test:bmad-installer-contract": "node tests/bmad-installer-contract-regressions.mjs",
|
|
35
37
|
"test:pjan-77": "node tests/pjan-77-notebook-domain-regressions.mjs && node tests/pjan-77-notebook-adapter-contract.mjs && node tests/pjan-77-notebook-lifecycle-regressions.mjs && node tests/pjan-77-notebook-cli-contract.mjs && node tests/pjan-77-notebook-hooks-capture.mjs && node tests/pjan-77-notebook-security-isolation.mjs && node tests/pjan-77-notebook-release-gates.mjs",
|
|
36
|
-
"test": "
|
|
38
|
+
"test": "node scripts/run-tests.mjs",
|
|
37
39
|
"migrate:up": "node-pg-migrate --migrations-dir migrations up",
|
|
38
40
|
"migrate:down": "node-pg-migrate --migrations-dir migrations down",
|
|
39
41
|
"migrate:create": "node-pg-migrate --migrations-dir migrations create",
|
|
40
42
|
"prepublishOnly": "npm run check:lock && npm run check:submodules -- --remote --recursive --archive --npm && npm run build && npm run check:tracked-secrets",
|
|
41
|
-
"test:hermes-profile-inheritance": "node tests/hermes-profile-inheritance-regressions.mjs"
|
|
43
|
+
"test:hermes-profile-inheritance": "node tests/hermes-profile-inheritance-regressions.mjs",
|
|
44
|
+
"test:coverage": "npm run build && NODE_OPTIONS=--max-old-space-size=12288 c8 --exclude='**/.pjan-*/**' --reporter=json-summary --reporter=text-summary --src=src --report-dir=coverage node scripts/run-tests.mjs",
|
|
45
|
+
"coverage:check": "node scripts/coverage-ratchet.mjs",
|
|
46
|
+
"coverage:apply": "node scripts/coverage-ratchet.mjs --apply"
|
|
42
47
|
},
|
|
43
48
|
"keywords": [
|
|
44
49
|
"cli",
|
|
@@ -60,6 +65,7 @@
|
|
|
60
65
|
"devDependencies": {
|
|
61
66
|
"@types/node": "^25.9.1",
|
|
62
67
|
"@types/pg": "^8.15.0",
|
|
68
|
+
"c8": "^10.1.3",
|
|
63
69
|
"esbuild": "^0.25.0",
|
|
64
70
|
"node-pg-migrate": "^8.0.0",
|
|
65
71
|
"typescript": "^5"
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
# 2. Creates ignored local state at agents/hermes/<role>/runtime/ and delegates
|
|
14
14
|
# named-profile wiring to `pj migrate hermes.runtime-singleton`
|
|
15
15
|
# 4. Creates a Plane project in 33god workspace
|
|
16
|
-
# 6.
|
|
17
|
-
# 7. Optionally
|
|
16
|
+
# 6. Verifies a BotFather token, stores it in 1Password, and maps an op:// reference
|
|
17
|
+
# 7. Optionally does the same for a dedicated Slack app+bot pair
|
|
18
18
|
# 8. Records fleet-shared Bloodbank routing identity (no profile consumer)
|
|
19
19
|
# 9. Installs a profile gateway + heartbeat timer (reconcile + checkpoint)
|
|
20
20
|
# 10. Appends to ~/.hermes/agents-registry.yaml
|
|
@@ -34,6 +34,10 @@ _envops:
|
|
|
34
34
|
variable_end_string: "}}"
|
|
35
35
|
comment_start_string: "{#"
|
|
36
36
|
comment_end_string: "#}"
|
|
37
|
+
_exclude:
|
|
38
|
+
- "**/__pycache__"
|
|
39
|
+
- "**/*.pyc"
|
|
40
|
+
- "**/*.pyo"
|
|
37
41
|
|
|
38
42
|
# ─── Questions (only what we can't derive) ───────────────────────────────
|
|
39
43
|
|
|
@@ -111,6 +115,11 @@ soul_tone:
|
|
|
111
115
|
- formal
|
|
112
116
|
- terse
|
|
113
117
|
|
|
118
|
+
reconcile_enabled:
|
|
119
|
+
type: bool
|
|
120
|
+
help: "Run autonomous PM ticket-board reconciliation from the heartbeat timer"
|
|
121
|
+
default: true
|
|
122
|
+
|
|
114
123
|
# ─── Derived (never asked) ───────────────────────────────────────────────
|
|
115
124
|
|
|
116
125
|
agent_id:
|
|
@@ -169,5 +178,9 @@ _tasks:
|
|
|
169
178
|
- "./.scripts/99-summary.sh"
|
|
170
179
|
|
|
171
180
|
_skip_if_exists:
|
|
172
|
-
|
|
181
|
+
# role.yaml contains verified channel identities and explicit operator
|
|
182
|
+
# choices (including reconcile_enabled=false). A forced Copier refresh must
|
|
183
|
+
# preserve those durable values; the idempotent tasks below reconcile any
|
|
184
|
+
# newly-required fields and lifecycle assets around them.
|
|
185
|
+
- role.yaml
|
|
173
186
|
- runtime
|
|
@@ -8,7 +8,7 @@ short and high-signal — anything that should ALWAYS be in context.
|
|
|
8
8
|
You are **{{display_name}}**, the `{{role}}` agent for the `{{repo}}` repo.
|
|
9
9
|
Your scope is the repo at the project root. You communicate via Telegram
|
|
10
10
|
(`@{{repo}}_{{role}}_bot`) and canonical Bloodbank repo events on
|
|
11
|
-
`bloodbank.evt.
|
|
11
|
+
`bloodbank.evt.repo.>`, filtered by `data.repo = {{repo}}`.
|
|
12
12
|
|
|
13
13
|
## Operator
|
|
14
14
|
|
|
@@ -37,9 +37,12 @@ preambles.
|
|
|
37
37
|
|
|
38
38
|
Always emit envelope-shaped events with `actor.agent_id = {{agent_id}}` and
|
|
39
39
|
`producer = hermes-agent:{{agent_id}}` for every consequential action.
|
|
40
|
-
Event type format: `bloodbank
|
|
41
|
-
lowercase, past-tense action for events).
|
|
42
|
-
|
|
40
|
+
Event type format: `bloodbank.<domain>.<entity>.<action>` (4 tokens,
|
|
41
|
+
lowercase, past-tense action for events). The NATS subject inserts a kind
|
|
42
|
+
marker as segment 2 — `bloodbank.<evt|cmd|rpy>.<domain>.<entity>.<action>`
|
|
43
|
+
(5 tokens). There is no version token in either shape; a breaking payload
|
|
44
|
+
change earns a new action or entity, never a `v<n>` segment. Naming spec
|
|
45
|
+
lives at `~/code/33GOD/bloodbank/docs/event-naming.md`.
|
|
43
46
|
|
|
44
47
|
## Recent context
|
|
45
48
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Create the initial named Hermes profile
|
|
2
|
+
# Create or reconcile the initial named Hermes profile without cloning secrets.
|
|
3
3
|
# The directory remains REAL. Step 20 delegates final shared-vs-owned link
|
|
4
4
|
# topology to `pj migrate hermes.runtime-singleton`; no template step may
|
|
5
5
|
# replace the profile itself with a symlink.
|
|
@@ -13,104 +13,104 @@ fi
|
|
|
13
13
|
source "$(dirname "$0")/_lib.sh"
|
|
14
14
|
load_role_env
|
|
15
15
|
|
|
16
|
-
|
|
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[*]}"
|
|
17
66
|
|
|
18
67
|
log "[10] creating hermes profile: $PROFILE_NAME"
|
|
19
|
-
PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
|
|
20
68
|
|
|
21
69
|
if [[ -d "$PROFILE_HOME" ]]; then
|
|
22
70
|
log " profile dir already exists; reusing"
|
|
23
71
|
else
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
"$HERMES_BIN" profile create "$PROFILE_NAME" --clone --no-alias
|
|
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
|
|
29
76
|
fi
|
|
30
77
|
|
|
31
|
-
# Manually copy the inheritable bits that --clone doesn't get.
|
|
32
|
-
# These are content-only dirs; safe to mirror without recursion risk.
|
|
33
|
-
log " mirroring skills, plugins, hooks from default profile"
|
|
34
|
-
for sub in skills plugins hooks cron skins; do
|
|
35
|
-
src="$HOME/.hermes/$sub"
|
|
36
|
-
dst="$PROFILE_HOME/$sub"
|
|
37
|
-
if [[ -d "$src" && "$src" != "$PROFILE_HOME"* ]]; then
|
|
38
|
-
mkdir -p "$dst"
|
|
39
|
-
# cp -R, dereferencing symlinks; -u to preserve newer if dst exists
|
|
40
|
-
cp -RLu "$src/." "$dst/" 2>/dev/null || cp -RL "$src/." "$dst/" 2>/dev/null || true
|
|
41
|
-
fi
|
|
42
|
-
done
|
|
43
|
-
|
|
44
78
|
# Strip any inherited gateway/runtime state so this profile boots clean.
|
|
45
79
|
rm -f "$PROFILE_HOME/gateway.pid" "$PROFILE_HOME/gateway_state.json" \
|
|
46
80
|
"$PROFILE_HOME/processes.json" "$PROFILE_HOME/state.db" 2>/dev/null || true
|
|
47
81
|
# Belt-and-suspenders: if a profiles/ dir somehow exists, remove it
|
|
48
82
|
[[ -d "$PROFILE_HOME/profiles" ]] && rm -rf "$PROFILE_HOME/profiles"
|
|
49
83
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# parent's Slack/Telegram socket if it ever connects, and (b) crash-loop: the
|
|
55
|
-
# gateway treats an inherited-but-unusable platform as "configured", fails to
|
|
56
|
-
# connect it, and exits non-fatal only when ZERO platforms are configured. A
|
|
57
|
-
# sub-agent must establish its OWN identity via the Wire steps (30-telegram etc),
|
|
58
|
-
# never borrow the parent's.
|
|
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.
|
|
59
88
|
PROFILE_ENV="$PROFILE_HOME/.env"
|
|
60
89
|
if [[ -f "$PROFILE_ENV" ]]; then
|
|
61
|
-
# Some Hermes versions implement `profile create --clone` by symlinking the
|
|
62
|
-
# cloned .env back to the default profile. Never sanitize through that
|
|
63
|
-
# symlink: doing so would delete the operator's fleet-wide provider secrets.
|
|
64
|
-
# Break only the .env link into a private, mode-0600 copy first.
|
|
65
|
-
if [[ -L "$PROFILE_ENV" ]]; then
|
|
66
|
-
log " detaching cloned .env symlink before profile-local sanitization"
|
|
67
|
-
PROFILE_ENV_COPY="$PROFILE_HOME/.env.profile-local.$$"
|
|
68
|
-
cp -L "$PROFILE_ENV" "$PROFILE_ENV_COPY"
|
|
69
|
-
chmod 600 "$PROFILE_ENV_COPY"
|
|
70
|
-
mv -fT "$PROFILE_ENV_COPY" "$PROFILE_ENV"
|
|
71
|
-
fi
|
|
72
|
-
log " stripping inherited platform credentials from profile .env"
|
|
73
90
|
python3 - "$PROFILE_ENV" <<'PYEOF'
|
|
74
|
-
import
|
|
91
|
+
import sys, pathlib
|
|
75
92
|
p = pathlib.Path(sys.argv[1])
|
|
76
|
-
# Identity-bearing platform creds that must be per-agent, not inherited.
|
|
77
93
|
keys = (
|
|
78
|
-
"SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "
|
|
79
|
-
"
|
|
80
|
-
"TELEGRAM_BOT_TOKEN", "TELEGRAM_ALLOWED_USERS",
|
|
81
|
-
"DISCORD_BOT_TOKEN", "DISCORD_ALLOWED_USERS", "DISCORD_HOME_CHANNEL",
|
|
82
|
-
"DISCORD_HOME_CHANNEL_NAME",
|
|
94
|
+
"SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_SIGNING_SECRET",
|
|
95
|
+
"TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN",
|
|
83
96
|
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
)
|
|
90
112
|
PYEOF
|
|
91
|
-
chmod 600 "$PROFILE_ENV"
|
|
92
|
-
fi
|
|
93
|
-
|
|
94
|
-
if [[ "$ROLE" == "pm" ]]; then
|
|
95
|
-
VOX_URL_VALUE="${VOX_URL:-$(config_get fleet.vox_url 'https://vox.delo.sh')}"
|
|
96
|
-
if [[ -n "$VOX_URL_VALUE" ]]; then
|
|
97
|
-
log " ensuring VOX_URL is present for PM voice mode"
|
|
98
|
-
python3 - "$PROFILE_ENV" "$VOX_URL_VALUE" <<'PYEOF'
|
|
99
|
-
import pathlib, sys
|
|
100
|
-
|
|
101
|
-
path = pathlib.Path(sys.argv[1])
|
|
102
|
-
vox_url = sys.argv[2]
|
|
103
|
-
lines = path.read_text().splitlines() if path.exists() else []
|
|
104
|
-
for idx, line in enumerate(lines):
|
|
105
|
-
if line.startswith("VOX_URL="):
|
|
106
|
-
lines[idx] = f'VOX_URL="{vox_url}"'
|
|
107
|
-
break
|
|
108
|
-
else:
|
|
109
|
-
lines.append(f'VOX_URL="{vox_url}"')
|
|
110
|
-
path.write_text("\n".join(lines) + "\n")
|
|
111
|
-
PYEOF
|
|
112
|
-
chmod 600 "$PROFILE_ENV"
|
|
113
|
-
fi
|
|
113
|
+
[[ -L "$PROFILE_ENV" ]] || chmod 600 "$PROFILE_ENV"
|
|
114
114
|
fi
|
|
115
115
|
|
|
116
116
|
# Never persist a project-specific terminal.cwd through the named profile.
|
|
@@ -127,15 +127,13 @@ fi
|
|
|
127
127
|
# Seed an EMPTY delta: a new agent should be identical to the fleet base, and
|
|
128
128
|
# every line here is an override someone must justify later.
|
|
129
129
|
PROFILE_DELTA="$PROFILE_HOME/config.delta.yaml"
|
|
130
|
-
|
|
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
|
|
131
134
|
log " seeding empty config.delta.yaml (override-only SSOT)"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# Merged over ~/.hermes/config.yaml to produce config.yaml (which is GENERATED).
|
|
135
|
-
# Empty == identical to the fleet base. Add ONLY what must differ.
|
|
136
|
-
{}
|
|
137
|
-
DELTA_EOF
|
|
138
|
-
chmod 600 "$PROFILE_DELTA"
|
|
135
|
+
elif [[ "$profile_delta_seed_result" != "exists" ]]; then
|
|
136
|
+
die "profile config seed helper returned an invalid result"
|
|
139
137
|
fi
|
|
140
138
|
|
|
141
139
|
# Pin the identity-memory bank explicitly rather than relying on the fleet
|
|
@@ -166,7 +164,6 @@ else
|
|
|
166
164
|
fi
|
|
167
165
|
|
|
168
166
|
# Canonical shared-skill source of truth + local PM fallback sync.
|
|
169
|
-
CANONICAL_SKILLS_DIR="${CANONICAL_SKILLS_DIR:-$(config_get fleet.canonical_skills_dir '/home/delorenj/.agents/skills')}"
|
|
170
167
|
CANONICAL_PM_SKILL_SRC="$CANONICAL_SKILLS_DIR/subagent-driven-development"
|
|
171
168
|
LOCAL_PM_SKILL_DST="$PROFILE_HOME/skills/software-development/subagent-driven-development"
|
|
172
169
|
|
|
@@ -186,17 +183,14 @@ if [[ -d "$CANONICAL_SKILLS_DIR" ]]; then
|
|
|
186
183
|
|
|
187
184
|
# Ensure key PM/local-ops skills are symlinked into runtime/profile skills root.
|
|
188
185
|
# This preserves canonical ownership and keeps updates instant across agents.
|
|
189
|
-
read -r -a SYMLINKED_RUNTIME_SKILLS <<< "${SYMLINKED_RUNTIME_SKILLS:-$(config_get fleet.symlinked_runtime_skills 'delonet-conventions delonet-dotenv hermes-pm-template-maintenance hindsight subagent-driven-development')}"
|
|
190
186
|
mkdir -p "$PROFILE_HOME/skills"
|
|
191
187
|
|
|
192
188
|
for skill_name in "${SYMLINKED_RUNTIME_SKILLS[@]}"; do
|
|
193
189
|
src="$CANONICAL_SKILLS_DIR/$skill_name"
|
|
194
190
|
dst="$PROFILE_HOME/skills/$skill_name"
|
|
195
191
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
continue
|
|
199
|
-
fi
|
|
192
|
+
[[ -f "$src/SKILL.md" ]] \
|
|
193
|
+
|| die "required Skillex skill disappeared during provisioning: $src/SKILL.md"
|
|
200
194
|
|
|
201
195
|
if [[ -L "$dst" && "$(readlink "$dst")" == "$src" ]]; then
|
|
202
196
|
log " runtime skill symlink already set: $dst -> $src"
|
|
@@ -208,7 +202,7 @@ if [[ -d "$CANONICAL_SKILLS_DIR" ]]; then
|
|
|
208
202
|
log " symlinked runtime skill: $dst -> $src"
|
|
209
203
|
done
|
|
210
204
|
else
|
|
211
|
-
|
|
205
|
+
die "canonical Skillex projection directory disappeared during provisioning: $CANONICAL_SKILLS_DIR"
|
|
212
206
|
fi
|
|
213
207
|
|
|
214
208
|
if [[ -f "$CANONICAL_PM_SKILL_SRC/SKILL.md" ]]; then
|
|
@@ -216,7 +210,7 @@ if [[ -f "$CANONICAL_PM_SKILL_SRC/SKILL.md" ]]; then
|
|
|
216
210
|
mkdir -p "$LOCAL_PM_SKILL_DST"
|
|
217
211
|
cp -f "$CANONICAL_PM_SKILL_SRC/SKILL.md" "$LOCAL_PM_SKILL_DST/SKILL.md"
|
|
218
212
|
else
|
|
219
|
-
|
|
213
|
+
die "required canonical PM skill disappeared during provisioning: $CANONICAL_PM_SKILL_SRC/SKILL.md"
|
|
220
214
|
fi
|
|
221
215
|
|
|
222
216
|
# Install the project's SOUL.md into the profile so the agent loads it.
|
|
@@ -70,7 +70,25 @@ PYEOF
|
|
|
70
70
|
# Never let a literal secret reach the runtime. The scaffold is rendered from
|
|
71
71
|
# templates, so a leaked credential shows up here before anything is copied.
|
|
72
72
|
python3 "$(dirname "$0")/secret-scan.py" "$TMP"
|
|
73
|
-
|
|
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
|
|
74
92
|
|
|
75
93
|
# Seed mutable identity/config only when no local value exists.
|
|
76
94
|
if [[ "$ROLE" == "reporter" ]]; then
|
|
@@ -148,29 +166,34 @@ log " singleton profile verified by pj migrate hermes.runtime-singleton: $PRO
|
|
|
148
166
|
# config.yaml is fleet-shared in the singleton topology. The manual and
|
|
149
167
|
# service launchers pass TERMINAL_CWD process-locally for this role instead.
|
|
150
168
|
|
|
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
|
-
}
|
|
159
|
-
|
|
160
169
|
if [[ "$ROLE" == "pm" ]]; then
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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"
|
|
166
194
|
else
|
|
167
|
-
|
|
195
|
+
log " optional Vox plugin not installed; PM voice activation deferred"
|
|
168
196
|
fi
|
|
169
|
-
|
|
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"
|
|
174
197
|
fi
|
|
175
198
|
|
|
176
199
|
mark_done 20-runtime-repo
|