@delorenj/pjangler 1.2.33 → 1.3.7
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 +72 -0
- package/dist/index.js +4754 -2564
- package/dist/mcp-server.js +4516 -2141
- package/dist/prompt.js +404 -0
- package/package.json +7 -5
- package/templates/commonproject/copier.yml +6 -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/scripts/provision-packs.py +14 -50
- package/templates/commonproject/template/mise.toml.jinja +11 -11
- package/templates/hermes-agent/copier.yml +34 -14
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
- package/templates/hermes-agent/template/.scripts/01-config.sh +13 -4
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +73 -14
- 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/42-ticket-provider.sh +18 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +114 -24
- package/templates/hermes-agent/template/.scripts/80-registry.sh +19 -3
- package/templates/hermes-agent/template/.scripts/_lib.sh +74 -11
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
- 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 +15 -2
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +734 -0
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +31 -5
- package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
- package/templates/hermes-agent/template/SOUL.md.jinja +48 -12
- package/templates/hermes-agent/template/hermes.jinja +51 -10
- package/templates/hermes-agent/template/momo.jinja +177 -0
- package/templates/hermes-agent/template/role.yaml.jinja +31 -21
|
@@ -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,6 +12,15 @@
|
|
|
12
12
|
# it becomes the SOT for every agent in this repo.
|
|
13
13
|
# Either way we register this agent under .project.json `agents` and mirror the
|
|
14
14
|
# binding into role.yaml for back-compat (80-registry.sh / 99-summary.sh).
|
|
15
|
+
|
|
16
|
+
# The caller's negative board grant is authoritative. Check it before sourcing
|
|
17
|
+
# _lib.sh so a skipped step cannot load fleet credentials, create logs/markers,
|
|
18
|
+
# inspect bindings, or reach a provider adapter.
|
|
19
|
+
if [[ "${SKIP_PLANE:-0}" == "1" ]]; then
|
|
20
|
+
printf '%s\n' '[42] ticket provider — SKIPPED (SKIP_PLANE=1)' >&2
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
15
24
|
# shellcheck source=_lib.sh
|
|
16
25
|
source "$(dirname "$0")/_lib.sh"
|
|
17
26
|
load_role_env
|
|
@@ -74,7 +83,15 @@ if set_provider == "1":
|
|
|
74
83
|
if board_url: tp["board_url"] = board_url
|
|
75
84
|
if team: tp["team"] = team
|
|
76
85
|
ag = d.setdefault("agents", {})
|
|
77
|
-
|
|
86
|
+
entry = ag.get(os.environ["AGENT_ID"], {})
|
|
87
|
+
if not isinstance(entry, dict):
|
|
88
|
+
entry = {}
|
|
89
|
+
entry.update({
|
|
90
|
+
"role": os.environ["ROLE"],
|
|
91
|
+
"role_dir": os.environ["ROLE_DIR_REL"],
|
|
92
|
+
"provisioning_state": "provisioned",
|
|
93
|
+
})
|
|
94
|
+
ag[os.environ["AGENT_ID"]] = entry
|
|
78
95
|
p.write_text(json.dumps(d, indent=2) + "\n")
|
|
79
96
|
PY
|
|
80
97
|
log " .project.json updated (agent=$AGENT_ID)"
|
|
@@ -12,9 +12,88 @@ 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
|
+
# Render every caller-controlled systemd scalar through the same data-only
|
|
41
|
+
# serializer used by fleet backfill. This validation happens before mkdir,
|
|
42
|
+
# systemctl, or unit writes, so CR/LF/NUL cannot create a second directive and
|
|
43
|
+
# spaces/quotes/backslashes/specifier bytes remain exact.
|
|
44
|
+
PYTHON_BIN="$(builtin type -P python3)" || die "python3 is unavailable for systemd unit serialization"
|
|
45
|
+
systemd_value() {
|
|
46
|
+
"$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-value "$1" \
|
|
47
|
+
|| die "systemd value validation failed"
|
|
48
|
+
}
|
|
49
|
+
systemd_environment() {
|
|
50
|
+
"$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-environment "$1" "$2" \
|
|
51
|
+
|| die "systemd environment validation failed"
|
|
52
|
+
}
|
|
53
|
+
systemd_exec_value() {
|
|
54
|
+
"$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-exec-value "$1" \
|
|
55
|
+
|| die "systemd ExecStart value validation failed"
|
|
56
|
+
}
|
|
57
|
+
GW_DESCRIPTION="$(systemd_value "Hermes Gateway — $DISPLAY_NAME")"
|
|
58
|
+
HB_DESCRIPTION="$(systemd_value "Hermes Heartbeat (reconcile + checkpoint) — $DISPLAY_NAME")"
|
|
59
|
+
TIMER_DESCRIPTION="$(systemd_value "Heartbeat (reconcile + checkpoint) for $AGENT_ID")"
|
|
60
|
+
ENV_HERMES_HOME="$(systemd_environment HERMES_HOME "$PROFILE_HOME")"
|
|
61
|
+
ENV_HERMES_BIN="$(systemd_environment HERMES_BIN "$HERMES_BIN")"
|
|
62
|
+
ENV_CODEX_HOME="$(systemd_environment CODEX_HOME "$CODEX_HOME")"
|
|
63
|
+
ENV_TERMINAL_CWD="$(systemd_environment TERMINAL_CWD "$REPO_ROOT")"
|
|
64
|
+
WORKING_DIRECTORY="$(systemd_value "$REPO_ROOT")"
|
|
65
|
+
RUNTIME_ENV_FILE="$(systemd_value "-$RUNTIME/.env")"
|
|
66
|
+
GW_LOG_OUTPUT="$(systemd_value "append:$RUNTIME/logs/gateway.systemd.log")"
|
|
67
|
+
HB_LOG_OUTPUT="$(systemd_value "append:$RUNTIME/logs/heartbeat.log")"
|
|
68
|
+
GW_EXEC_START="$(systemd_exec_value "$ROLE_DIR/.scripts/credential-launch.sh")"
|
|
69
|
+
HB_EXEC_START="$GW_EXEC_START"
|
|
70
|
+
|
|
71
|
+
# Encrypted credentials are optional and take precedence over the existing
|
|
72
|
+
# ignored runtime/.env fallback. Validate every path-derived unit scalar before
|
|
73
|
+
# any directory, unit, marker, or systemd mutation.
|
|
74
|
+
CREDENTIAL_DIR="${HERMES_SYSTEMD_CREDENTIAL_DIR:-$HOME/.config/hermes-agent/credentials}"
|
|
75
|
+
TELEGRAM_CREDENTIAL="$CREDENTIAL_DIR/${AGENT_ID}-telegram-bot-token.cred"
|
|
76
|
+
MODEL_CREDENTIAL="$CREDENTIAL_DIR/${AGENT_ID}-model-api-key.cred"
|
|
77
|
+
GW_CREDENTIAL_LINES=""
|
|
78
|
+
HB_CREDENTIAL_LINES=""
|
|
79
|
+
if [[ -f "$TELEGRAM_CREDENTIAL" ]]; then
|
|
80
|
+
command -v systemd-creds >/dev/null 2>&1 \
|
|
81
|
+
|| die "encrypted Telegram credential exists but systemd-creds is unavailable"
|
|
82
|
+
GW_CREDENTIAL_LINES="LoadCredentialEncrypted=$(systemd_value "telegram_bot_token:$TELEGRAM_CREDENTIAL")"
|
|
83
|
+
fi
|
|
84
|
+
if [[ -f "$MODEL_CREDENTIAL" ]]; then
|
|
85
|
+
[[ -n "$(yaml_get model.key_env)" ]] \
|
|
86
|
+
|| die "encrypted model credential exists but model.key_env is blank in role.yaml"
|
|
87
|
+
command -v systemd-creds >/dev/null 2>&1 \
|
|
88
|
+
|| die "encrypted model credential exists but systemd-creds is unavailable"
|
|
89
|
+
GW_CREDENTIAL_LINES="${GW_CREDENTIAL_LINES}${GW_CREDENTIAL_LINES:+$'\n'}LoadCredentialEncrypted=$(systemd_value "model_api_key:$MODEL_CREDENTIAL")"
|
|
90
|
+
HB_CREDENTIAL_LINES="LoadCredentialEncrypted=$(systemd_value "model_api_key:$MODEL_CREDENTIAL")"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Singleton-runtime contract: units set HERMES_HOME to the agent's NAMED PROFILE
|
|
94
|
+
# dir, never the raw runtime path — Hermes derives profile identity and shared
|
|
95
|
+
# fleet auth from the unresolved HERMES_HOME. $RUNTIME stays correct for
|
|
96
|
+
# EnvironmentFile and logs: those are the repo-owned side the profile links to.
|
|
18
97
|
mkdir -p "$SYS_DIR" "$RUNTIME/logs"
|
|
19
98
|
|
|
20
99
|
# Upgrade remediation must run before honoring a legacy done marker. Older
|
|
@@ -56,34 +135,45 @@ if [[ $legacy_consumer_present -eq 1 ]]; then
|
|
|
56
135
|
log " retired legacy per-profile Bloodbank consumer: $LEGACY_CONSUMER_UNIT"
|
|
57
136
|
fi
|
|
58
137
|
|
|
59
|
-
already_done 70-systemd
|
|
60
|
-
[[ "$
|
|
138
|
+
if already_done 70-systemd; then
|
|
139
|
+
if [[ -f "$SYS_DIR/$GW_UNIT" && -f "$SYS_DIR/$HB_SVC" && -f "$SYS_DIR/$HB_TIMER" ]]; then
|
|
140
|
+
log "[70] systemd already installed — reconciling unit definitions"
|
|
141
|
+
else
|
|
142
|
+
clear_done 70-systemd
|
|
143
|
+
log " stale/incomplete systemd marker cleared — reconciling required units"
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
61
146
|
|
|
62
147
|
# The heartbeat runner (board-reconciliation sentinel pass + gated checkpoint)
|
|
63
148
|
# and the checkpoint helper both render into the role dir; just ensure they are
|
|
64
149
|
# executable. heartbeat.sh calls checkpoint.sh internally.
|
|
65
150
|
HEARTBEAT_BIN="$ROLE_DIR/.scripts/heartbeat.sh"
|
|
66
|
-
|
|
151
|
+
CREDENTIAL_LAUNCHER="$ROLE_DIR/.scripts/credential-launch.sh"
|
|
152
|
+
chmod +x "$HEARTBEAT_BIN" "$CREDENTIAL_LAUNCHER" "$ROLE_DIR/.scripts/checkpoint.sh" 2>/dev/null || true
|
|
153
|
+
|
|
154
|
+
[[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]] \
|
|
155
|
+
|| die "named profile is not a real directory; run: pj migrate hermes.runtime-singleton '$REPO_ROOT'"
|
|
67
156
|
|
|
68
157
|
# Gateway unit
|
|
69
|
-
GW_UNIT="hermes-${AGENT_ID}-gateway.service"
|
|
70
158
|
cat > "$SYS_DIR/$GW_UNIT" <<UNIT
|
|
71
159
|
[Unit]
|
|
72
|
-
Description
|
|
160
|
+
Description=$GW_DESCRIPTION
|
|
73
161
|
After=network-online.target
|
|
74
162
|
Wants=network-online.target
|
|
75
163
|
|
|
76
164
|
[Service]
|
|
77
165
|
Type=simple
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
166
|
+
$ENV_HERMES_HOME
|
|
167
|
+
$ENV_HERMES_BIN
|
|
168
|
+
$ENV_CODEX_HOME
|
|
169
|
+
$ENV_TERMINAL_CWD
|
|
170
|
+
EnvironmentFile=$RUNTIME_ENV_FILE
|
|
171
|
+
$GW_CREDENTIAL_LINES
|
|
172
|
+
ExecStart=$GW_EXEC_START gateway
|
|
83
173
|
Restart=on-failure
|
|
84
174
|
RestartSec=10
|
|
85
|
-
StandardOutput
|
|
86
|
-
StandardError
|
|
175
|
+
StandardOutput=$GW_LOG_OUTPUT
|
|
176
|
+
StandardError=$GW_LOG_OUTPUT
|
|
87
177
|
|
|
88
178
|
[Install]
|
|
89
179
|
WantedBy=default.target
|
|
@@ -93,33 +183,33 @@ UNIT
|
|
|
93
183
|
# Frequent ticks (1 min); heartbeat.sh's own cooldown/lock logic rate-limits the
|
|
94
184
|
# full Hermes pass, and the checkpoint is gated to ~hourly inside the runner.
|
|
95
185
|
# 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
186
|
cat > "$SYS_DIR/$HB_SVC" <<UNIT
|
|
99
187
|
[Unit]
|
|
100
|
-
Description
|
|
188
|
+
Description=$HB_DESCRIPTION
|
|
101
189
|
After=network-online.target
|
|
102
190
|
Wants=network-online.target
|
|
103
191
|
|
|
104
192
|
[Service]
|
|
105
193
|
Type=oneshot
|
|
106
|
-
WorkingDirectory=$
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
194
|
+
WorkingDirectory=$WORKING_DIRECTORY
|
|
195
|
+
$ENV_HERMES_HOME
|
|
196
|
+
$ENV_HERMES_BIN
|
|
197
|
+
$ENV_CODEX_HOME
|
|
198
|
+
$ENV_TERMINAL_CWD
|
|
110
199
|
EnvironmentFile=-%h/.config/hermes-agent/env
|
|
111
200
|
EnvironmentFile=-%h/.hermes/env
|
|
112
201
|
EnvironmentFile=-%h/.hermes/hermes-agent.env
|
|
113
202
|
EnvironmentFile=-%h/.hermes/${AGENT_ID}.env
|
|
114
|
-
EnvironmentFile
|
|
115
|
-
|
|
203
|
+
EnvironmentFile=$RUNTIME_ENV_FILE
|
|
204
|
+
$HB_CREDENTIAL_LINES
|
|
205
|
+
ExecStart=$HB_EXEC_START heartbeat
|
|
116
206
|
TimeoutStartSec=45min
|
|
117
|
-
StandardOutput
|
|
118
|
-
StandardError
|
|
207
|
+
StandardOutput=$HB_LOG_OUTPUT
|
|
208
|
+
StandardError=$HB_LOG_OUTPUT
|
|
119
209
|
UNIT
|
|
120
210
|
cat > "$SYS_DIR/$HB_TIMER" <<UNIT
|
|
121
211
|
[Unit]
|
|
122
|
-
Description
|
|
212
|
+
Description=$TIMER_DESCRIPTION
|
|
123
213
|
|
|
124
214
|
[Timer]
|
|
125
215
|
OnBootSec=1min
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Append this agent's entry to the global fleet registry.
|
|
3
|
+
|
|
4
|
+
if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]; then
|
|
5
|
+
printf '%s\n' '[80] fleet registry — DEFERRED (SKIP_HOST_STATE=1)' >&2
|
|
6
|
+
exit 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
3
9
|
# shellcheck source=_lib.sh
|
|
4
10
|
source "$(dirname "$0")/_lib.sh"
|
|
5
11
|
load_role_env
|
|
@@ -29,7 +35,8 @@ python3 - "$REGISTRY_FILE" "$AGENT_ID" "$REPO" "$ROLE" "$DISPLAY_NAME" \
|
|
|
29
35
|
"$(yaml_get slack.provisioning_status)" "$(yaml_get slack.team_id)" \
|
|
30
36
|
"$(yaml_get slack.team_name)" "$(yaml_get slack.bot_user_id)" \
|
|
31
37
|
"$(yaml_get slack.bot_id)" "$(yaml_get slack.bot_username)" \
|
|
32
|
-
"$(yaml_get bloodbank.
|
|
38
|
+
"$(yaml_get bloodbank.enabled)" "$(yaml_get bloodbank.gateway_scope)" \
|
|
39
|
+
"$(yaml_get bloodbank.target_agent_id)" \
|
|
33
40
|
"$PLANE_WORKSPACE" "$PLANE_PROJECT_ID" "$(yaml_get plane.identifier)" \
|
|
34
41
|
"$RUNTIME_REPO" "$HERMES_BIN" "$HERMES_AGENT_REPO" "$HERMES_RUNTIME_GIT_URL" \
|
|
35
42
|
"$HERMES_RUNTIME_GIT_REF" "$HERMES_RUNTIME_GIT_SHA" "$FLEET_ENV" \
|
|
@@ -47,13 +54,21 @@ except ImportError:
|
|
|
47
54
|
(path, agent_id, repo, role, display, project, role_dir, profile,
|
|
48
55
|
telegram_status, bot, telegram_bot_id,
|
|
49
56
|
slack_status, slack_team_id, slack_team_name, slack_user_id, slack_bot_id,
|
|
50
|
-
slack_username, bloodbank_scope, bloodbank_target, plane_ws, plane_id,
|
|
57
|
+
slack_username, bloodbank_enabled, bloodbank_scope, bloodbank_target, plane_ws, plane_id,
|
|
51
58
|
plane_ident, runtime_repo, hermes_bin, hermes_repo, hermes_git_url,
|
|
52
|
-
hermes_git_ref, hermes_git_sha, fleet_env, gw, heartbeat) = sys.argv[1:
|
|
59
|
+
hermes_git_ref, hermes_git_sha, fleet_env, gw, heartbeat) = sys.argv[1:33]
|
|
53
60
|
p = pathlib.Path(path)
|
|
54
61
|
if p.is_symlink():
|
|
55
62
|
raise SystemExit(f"refusing to update registry symlink: {p}")
|
|
56
63
|
data = yaml.safe_load(p.read_text()) or {"schema_version": 1, "agents": {}}
|
|
64
|
+
if bloodbank_enabled == "":
|
|
65
|
+
bloodbank_enabled_value = False
|
|
66
|
+
elif bloodbank_enabled == "true":
|
|
67
|
+
bloodbank_enabled_value = True
|
|
68
|
+
elif bloodbank_enabled == "false":
|
|
69
|
+
bloodbank_enabled_value = False
|
|
70
|
+
else:
|
|
71
|
+
raise SystemExit("bloodbank.enabled must be the strict YAML boolean true or false")
|
|
57
72
|
data.setdefault("agents", {})[agent_id] = {
|
|
58
73
|
"repo": repo, "role": role, "display_name": display,
|
|
59
74
|
"project_path": project, "role_dir": role_dir,
|
|
@@ -72,6 +87,7 @@ data.setdefault("agents", {})[agent_id] = {
|
|
|
72
87
|
"bot_username": slack_username,
|
|
73
88
|
},
|
|
74
89
|
"bloodbank": {
|
|
90
|
+
"enabled": bloodbank_enabled_value,
|
|
75
91
|
"gateway_scope": bloodbank_scope,
|
|
76
92
|
"target_agent_id": bloodbank_target,
|
|
77
93
|
},
|
|
@@ -137,13 +137,57 @@ already_done() {
|
|
|
137
137
|
mark_done() {
|
|
138
138
|
touch "$ROLE_DIR/.scripts/.done-$1"
|
|
139
139
|
}
|
|
140
|
+
clear_done() {
|
|
141
|
+
# Remove only the marker for the explicitly deferred step. This lets a
|
|
142
|
+
# later activation reconcile that phase without disturbing completed,
|
|
143
|
+
# unrelated provisioning state.
|
|
144
|
+
rm -f -- "$ROLE_DIR/.scripts/.done-$1"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# The parser protocol, unsafe-family scrub, complete framing validation, and
|
|
148
|
+
# atomic environment apply live in one reusable library shared by provisioners,
|
|
149
|
+
# launchers, and maintenance commands.
|
|
150
|
+
FLEET_ENV_LIBRARY="$ROLE_DIR/.scripts/lib/fleet-env.sh"
|
|
151
|
+
FLEET_ENV_PARSER="$ROLE_DIR/.scripts/lib/parse-fleet-env.py"
|
|
152
|
+
if [[ ! -f "$FLEET_ENV_LIBRARY" || -L "$FLEET_ENV_LIBRARY" ]]; then
|
|
153
|
+
builtin printf 'fleet environment loader rejected: trusted library is unavailable\n' >&2
|
|
154
|
+
return 1
|
|
155
|
+
fi
|
|
156
|
+
# shellcheck source=lib/fleet-env.sh
|
|
157
|
+
builtin source "$FLEET_ENV_LIBRARY"
|
|
140
158
|
|
|
141
159
|
# Fleet source-of-truth (shared across all wrappers/provisioners).
|
|
142
160
|
# Every default below resolves as: env var > fleet.env > config.toml > fallback.
|
|
161
|
+
# Invocation authority is not fleet configuration. Capture the caller's board
|
|
162
|
+
# gate before importing fleet.env so that file cannot weaken an MCP/CLI
|
|
163
|
+
# SKIP_PLANE decision or re-enable credentials by assigning SKIP_PLANE=0.
|
|
164
|
+
PJANGLER_INVOCATION_SKIP_PLANE="${SKIP_PLANE:-0}"
|
|
143
165
|
FLEET_ENV="${HERMES_FLEET_ENV:-$(config_get fleet.fleet_env "$HOME/.hermes/fleet.env")}"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
|
|
167
|
+
# A direct CLI caller may not have passed through the MCP parent boundary. The
|
|
168
|
+
# shared loader hardens both before parser startup and again after atomic import.
|
|
169
|
+
if ! load_fleet_environment "$FLEET_ENV" "$FLEET_ENV_PARSER"; then
|
|
170
|
+
return 1
|
|
171
|
+
fi
|
|
172
|
+
SKIP_PLANE="$PJANGLER_INVOCATION_SKIP_PLANE"
|
|
173
|
+
unset PJANGLER_INVOCATION_SKIP_PLANE
|
|
174
|
+
scrub_subprocess_interpreter_injection
|
|
175
|
+
|
|
176
|
+
# fleet.env is allowed to supply provider credentials only for an explicitly
|
|
177
|
+
# board-authorized invocation. A no-board/deferred phase must remove every
|
|
178
|
+
# supported provider alias after sourcing and before any Python, Hermes,
|
|
179
|
+
# systemd, provider, or other child process can inherit it.
|
|
180
|
+
scrub_ticket_provider_authority() {
|
|
181
|
+
local key
|
|
182
|
+
unset PLANE_API_KEY TRELLO_KEY TRELLO_TOKEN LINEAR_API_KEY
|
|
183
|
+
while IFS= read -r key; do
|
|
184
|
+
case "$key" in
|
|
185
|
+
PLANE_*_API_KEY) unset "$key" ;;
|
|
186
|
+
esac
|
|
187
|
+
done < <(compgen -A variable PLANE_)
|
|
188
|
+
}
|
|
189
|
+
if [[ "$SKIP_PLANE" == "1" ]]; then
|
|
190
|
+
scrub_ticket_provider_authority
|
|
147
191
|
fi
|
|
148
192
|
# Identity-bearing chat credentials are never fleet-scoped. Platform wiring
|
|
149
193
|
# steps capture explicit invocation values before sourcing this library and
|
|
@@ -151,11 +195,12 @@ fi
|
|
|
151
195
|
unset TELEGRAM_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN
|
|
152
196
|
|
|
153
197
|
# Tools we expect on the host
|
|
154
|
-
HERMES_BIN="${HERMES_BIN:-${HERMES_FLEET_BIN:-$(config_get fleet.hermes_bin "$HOME/.
|
|
155
|
-
HERMES_AGENT_REPO="${HERMES_AGENT_REPO:-${HERMES_FLEET_REPO:-$(config_get fleet.hermes_repo "$HOME/.
|
|
198
|
+
HERMES_BIN="${HERMES_BIN:-${HERMES_FLEET_BIN:-$(config_get fleet.hermes_bin "$HOME/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1/.venv/bin/hermes")}}"
|
|
199
|
+
HERMES_AGENT_REPO="${HERMES_AGENT_REPO:-${HERMES_FLEET_REPO:-$(config_get fleet.hermes_repo "$HOME/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1")}}"
|
|
200
|
+
PJANGLER_BIN="${PJANGLER_BIN:-$(config_get fleet.pjangler_bin "pj")}"
|
|
156
201
|
HERMES_RUNTIME_GIT_URL="${HERMES_RUNTIME_GIT_URL:-$(config_get fleet.hermes_git_url 'https://github.com/delorenj/hermes-agent.git')}"
|
|
157
|
-
HERMES_RUNTIME_GIT_REF="${HERMES_RUNTIME_GIT_REF:-$(config_get fleet.hermes_git_ref '
|
|
158
|
-
HERMES_RUNTIME_GIT_SHA="${HERMES_RUNTIME_GIT_SHA:-$(config_get fleet.hermes_git_sha '
|
|
202
|
+
HERMES_RUNTIME_GIT_REF="${HERMES_RUNTIME_GIT_REF:-$(config_get fleet.hermes_git_ref 'main')}"
|
|
203
|
+
HERMES_RUNTIME_GIT_SHA="${HERMES_RUNTIME_GIT_SHA:-$(config_get fleet.hermes_git_sha '0408fec7a153e6c32c064acd2b8053917f1525f1')}"
|
|
159
204
|
HERMES_OAUTH_FILE="${HERMES_OAUTH_FILE:-${HERMES_FLEET_OAUTH_FILE:-$(config_get fleet.oauth_file "$HOME/.hermes/auth.json")}}"
|
|
160
205
|
CODEX_HOME="${CODEX_HOME:-${HERMES_FLEET_CODEX_HOME:-$(config_get fleet.codex_home "$HOME/.codex")}}"
|
|
161
206
|
# Prefer a scaffold vendored into this agent directory; fall back to the configured template path.
|
|
@@ -183,7 +228,7 @@ fleet_lock_acquire() {
|
|
|
183
228
|
fleet_lock_release() {
|
|
184
229
|
if [[ "${FLEET_LOCK_FD:-}" =~ ^[0-9]+$ ]]; then
|
|
185
230
|
flock -u "$FLEET_LOCK_FD" 2>/dev/null || true
|
|
186
|
-
|
|
231
|
+
exec {FLEET_LOCK_FD}>&-
|
|
187
232
|
fi
|
|
188
233
|
FLEET_LOCK_FD=""
|
|
189
234
|
}
|
|
@@ -195,13 +240,18 @@ BLOODBANK_COMPOSE_DIR="${BLOODBANK_COMPOSE_DIR:-$(config_get bloodbank.compose_d
|
|
|
195
240
|
|
|
196
241
|
# Plane
|
|
197
242
|
PLANE_BASE="${PLANE_BASE:-$(config_get plane.base 'https://plane.delo.sh')}"
|
|
198
|
-
|
|
243
|
+
if [[ "$SKIP_PLANE" != "1" ]]; then
|
|
244
|
+
PLANE_API_KEY="${PLANE_API_KEY:-${PLANE_33GOD_API_KEY:-}}"
|
|
245
|
+
fi
|
|
199
246
|
|
|
200
|
-
export FLEET_ENV HERMES_BIN HERMES_AGENT_REPO HERMES_RUNTIME_GIT_URL \
|
|
247
|
+
export FLEET_ENV HERMES_BIN HERMES_AGENT_REPO PJANGLER_BIN HERMES_RUNTIME_GIT_URL \
|
|
201
248
|
HERMES_RUNTIME_GIT_REF HERMES_RUNTIME_GIT_SHA HERMES_OAUTH_FILE CODEX_HOME \
|
|
202
249
|
RUNTIME_SCAFFOLD_DIR REGISTRY_FILE \
|
|
203
250
|
BLOODBANK_NATS_HOST BLOODBANK_NATS_PORT BLOODBANK_COMPOSE_DIR \
|
|
204
|
-
PLANE_BASE
|
|
251
|
+
PLANE_BASE SKIP_PLANE
|
|
252
|
+
if [[ "$SKIP_PLANE" != "1" ]]; then
|
|
253
|
+
export PLANE_API_KEY
|
|
254
|
+
fi
|
|
205
255
|
|
|
206
256
|
# systemd --user health check. Accept running/degraded/starting — only one
|
|
207
257
|
# broken unit shouldn't disqualify the rest of the user manager.
|
|
@@ -248,6 +298,19 @@ systemctl_user_unit_state() {
|
|
|
248
298
|
# Resolve project repo path (the repo that holds agents/hermes/<role>/).
|
|
249
299
|
# Walk up from $ROLE_DIR until we find a git root that isn't us.
|
|
250
300
|
project_repo_path() {
|
|
301
|
+
# Structured provisioners know the project root even before a fresh target
|
|
302
|
+
# receives its own .git directory. Accept only a root that contains this
|
|
303
|
+
# exact role path; otherwise fail closed instead of walking into a parent
|
|
304
|
+
# checkout and mutating its manifest.
|
|
305
|
+
if [[ -n "${PJANGLER_PROJECT_ROOT:-}" ]]; then
|
|
306
|
+
local explicit role_real
|
|
307
|
+
explicit="$(cd "$PJANGLER_PROJECT_ROOT" 2>/dev/null && pwd -P)" || return 1
|
|
308
|
+
role_real="$(cd "$ROLE_DIR" 2>/dev/null && pwd -P)" || return 1
|
|
309
|
+
case "$role_real" in
|
|
310
|
+
"$explicit"/agents/hermes/*) printf '%s\n' "$explicit"; return 0 ;;
|
|
311
|
+
*) return 1 ;;
|
|
312
|
+
esac
|
|
313
|
+
fi
|
|
251
314
|
local d="$ROLE_DIR"
|
|
252
315
|
[[ -d "$d/.git" || -f "$d/.git" ]] && { echo "$d"; return 0; }
|
|
253
316
|
for _ in 1 2 3 4 5; do
|
|
@@ -38,6 +38,23 @@ secret_scan_ok() {
|
|
|
38
38
|
return 0
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
# A checkpoint on a DETACHED HEAD belongs to no branch: `git push origin HEAD`
|
|
42
|
+
# cannot resolve a destination, so every commit stays local forever. One runtime
|
|
43
|
+
# reached 232 orphaned commits this way before anyone noticed.
|
|
44
|
+
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
|
|
45
|
+
if [ "$BRANCH" = "HEAD" ]; then
|
|
46
|
+
printf '[checkpoint] ABORT: %s is on a DETACHED HEAD — refusing to create commits that can never be pushed.\n' "$RUNTIME_DIR" >&2
|
|
47
|
+
printf '[checkpoint] Fix: git -C %s switch -c main && git -C %s push -u origin main\n' "$RUNTIME_DIR" "$RUNTIME_DIR" >&2
|
|
48
|
+
exit 4
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Report an existing backlog BEFORE adding to it, so a broken push is visible on
|
|
52
|
+
# the very next tick instead of compounding silently for months.
|
|
53
|
+
backlog="$(git rev-list --count "origin/$BRANCH..HEAD" 2>/dev/null || echo 0)"
|
|
54
|
+
if [ "$backlog" -ge 10 ]; then
|
|
55
|
+
printf '[checkpoint] WARNING: %s already has %s UNPUSHED commit(s) on %s.\n' "$RUNTIME_DIR" "$backlog" "$BRANCH" >&2
|
|
56
|
+
fi
|
|
57
|
+
|
|
41
58
|
git add -A
|
|
42
59
|
if git diff --cached --quiet; then
|
|
43
60
|
exit 0
|
|
@@ -51,4 +68,15 @@ if ! secret_scan_ok; then
|
|
|
51
68
|
fi
|
|
52
69
|
|
|
53
70
|
git -c commit.gpgsign=false commit -m "checkpoint $(date -Iseconds)" >/dev/null
|
|
54
|
-
|
|
71
|
+
|
|
72
|
+
# The push MUST be observable. This line used to be
|
|
73
|
+
# git push origin HEAD 2>&1 | tail -1 || true
|
|
74
|
+
# where `|| true` swallowed every failure — diverged branch, auth, network —
|
|
75
|
+
# and the pipe masked git's exit status on top of it. Run hourly, that turns a
|
|
76
|
+
# broken push into an invisible, unbounded pile of local-only commits.
|
|
77
|
+
if ! git push origin "$BRANCH" >/dev/null 2>&1; then
|
|
78
|
+
ahead="$(git rev-list --count "origin/$BRANCH..HEAD" 2>/dev/null || echo '?')"
|
|
79
|
+
printf '[checkpoint] PUSH FAILED for %s (%s commit(s) exist ONLY on this disk).\n' "$RUNTIME_DIR" "$ahead" >&2
|
|
80
|
+
printf '[checkpoint] Diagnose: git -C %s push origin %s\n' "$RUNTIME_DIR" "$BRANCH" >&2
|
|
81
|
+
exit 5
|
|
82
|
+
fi
|
|
@@ -12,14 +12,17 @@
|
|
|
12
12
|
|
|
13
13
|
[fleet]
|
|
14
14
|
# Absolute path to the shared Hermes executable every agent launcher execs.
|
|
15
|
-
hermes_bin = "~/.
|
|
15
|
+
hermes_bin = "~/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1/.venv/bin/hermes"
|
|
16
16
|
# Checkout of the shared hermes-agent repo.
|
|
17
|
-
hermes_repo = "~/.
|
|
17
|
+
hermes_repo = "~/.local/share/hermes-agent/releases/0408fec7a153e6c32c064acd2b8053917f1525f1"
|
|
18
|
+
# Canonical CommonProject migration CLI. Runtime provisioning delegates named
|
|
19
|
+
# profile topology to this command; it never rewrites profile links itself.
|
|
20
|
+
pjangler_bin = "pj"
|
|
18
21
|
# Reviewed fleet runtime publication. Provisioning metadata and clean installs
|
|
19
22
|
# must agree on this fork/ref/SHA; do not substitute upstream main.
|
|
20
23
|
hermes_git_url = "https://github.com/delorenj/hermes-agent.git"
|
|
21
|
-
hermes_git_ref = "
|
|
22
|
-
hermes_git_sha = "
|
|
24
|
+
hermes_git_ref = "main"
|
|
25
|
+
hermes_git_sha = "0408fec7a153e6c32c064acd2b8053917f1525f1"
|
|
23
26
|
# Fallback runtime scaffold dir — used only when an agent's vendored
|
|
24
27
|
# ./.runtime-scaffold is missing.
|
|
25
28
|
runtime_scaffold_dir = "~/code/hermes-agent-template/runtime-scaffold"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Secret-free systemd entrypoint. Decrypted values are read only from the
|
|
3
|
+
# service manager's volatile credential directory and never printed or written.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROLE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
+
ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
8
|
+
|
|
9
|
+
yaml_get() {
|
|
10
|
+
python3 - "$ROLE_YAML" "$1" <<'PYEOF'
|
|
11
|
+
import re, sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
text = Path(sys.argv[1]).read_text()
|
|
14
|
+
parts = sys.argv[2].split(".")
|
|
15
|
+
if len(parts) == 2:
|
|
16
|
+
match = re.search(
|
|
17
|
+
rf"(?m)^{re.escape(parts[0])}:[ \t]*\n((?:[ \t]+\S.*\n?)*)", text
|
|
18
|
+
)
|
|
19
|
+
text = match.group(1) if match else ""
|
|
20
|
+
key = parts[-1]
|
|
21
|
+
match = re.search(rf'(?m)^\s*{re.escape(key)}:\s*"?([^"\n]*)"?\s*$', text)
|
|
22
|
+
print(match.group(1).strip() if match else "")
|
|
23
|
+
PYEOF
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
AGENT_ID="$(yaml_get agent_id)"
|
|
27
|
+
PROFILE_NAME="$(yaml_get profile)"
|
|
28
|
+
[[ -n "$AGENT_ID" && -n "$PROFILE_NAME" ]] \
|
|
29
|
+
|| { printf 'credential-launch: incomplete role identity\n' >&2; exit 2; }
|
|
30
|
+
[[ -n "${HERMES_BIN:-}" && -x "$HERMES_BIN" ]] \
|
|
31
|
+
|| { printf 'credential-launch: HERMES_BIN is not executable\n' >&2; exit 1; }
|
|
32
|
+
|
|
33
|
+
mode="${1:-}"
|
|
34
|
+
[[ "$mode" == "gateway" || "$mode" == "heartbeat" ]] \
|
|
35
|
+
|| { printf 'credential-launch: expected gateway or heartbeat\n' >&2; exit 2; }
|
|
36
|
+
|
|
37
|
+
FLEET_HOME="${HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
38
|
+
PROFILE_HOME="$FLEET_HOME/profiles/${PROFILE_NAME:-$AGENT_ID}"
|
|
39
|
+
[[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]] \
|
|
40
|
+
|| { printf 'credential-launch: named profile is not a real directory\n' >&2; exit 1; }
|
|
41
|
+
export HERMES_HOME="$PROFILE_HOME"
|
|
42
|
+
|
|
43
|
+
if ! REPO_ROOT="$(git -C "$ROLE_DIR" rev-parse --show-toplevel 2>/dev/null)"; then
|
|
44
|
+
REPO_ROOT="$ROLE_DIR"
|
|
45
|
+
fi
|
|
46
|
+
export TERMINAL_CWD="$REPO_ROOT"
|
|
47
|
+
|
|
48
|
+
load_credential() {
|
|
49
|
+
local credential_id="$1" env_name="$2" credential_file value
|
|
50
|
+
[[ "$env_name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] \
|
|
51
|
+
|| { printf 'credential-launch: invalid credential environment name\n' >&2; exit 2; }
|
|
52
|
+
credential_file="${CREDENTIALS_DIRECTORY:-}/$credential_id"
|
|
53
|
+
[[ -n "${CREDENTIALS_DIRECTORY:-}" && -r "$credential_file" ]] || return 0
|
|
54
|
+
value="$(<"$credential_file")"
|
|
55
|
+
printf -v "$env_name" '%s' "$value"
|
|
56
|
+
export "$env_name"
|
|
57
|
+
unset value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
load_credential telegram_bot_token TELEGRAM_BOT_TOKEN
|
|
61
|
+
MODEL_KEY_ENV="$(yaml_get model.key_env)"
|
|
62
|
+
if [[ -n "$MODEL_KEY_ENV" ]]; then
|
|
63
|
+
load_credential model_api_key "$MODEL_KEY_ENV"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if [[ "$mode" == "heartbeat" ]]; then
|
|
67
|
+
exec "$ROLE_DIR/.scripts/heartbeat.sh"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
gateway_args=(gateway run --replace)
|
|
71
|
+
MODEL_NAME="$(yaml_get model.name)"
|
|
72
|
+
MODEL_PROVIDER="$(yaml_get model.provider)"
|
|
73
|
+
MODEL_BASE_URL="$(yaml_get model.base_url)"
|
|
74
|
+
MODEL_API_MODE="$(yaml_get model.api_mode)"
|
|
75
|
+
[[ -z "$MODEL_NAME" ]] || gateway_args+=(--model "$MODEL_NAME")
|
|
76
|
+
[[ -z "$MODEL_PROVIDER" ]] || gateway_args+=(--provider "$MODEL_PROVIDER")
|
|
77
|
+
[[ -z "$MODEL_BASE_URL" ]] || gateway_args+=(--base-url "$MODEL_BASE_URL")
|
|
78
|
+
[[ -z "$MODEL_API_MODE" ]] || gateway_args+=(--api-mode "$MODEL_API_MODE")
|
|
79
|
+
[[ -z "$MODEL_KEY_ENV" ]] || gateway_args+=(--key-env "$MODEL_KEY_ENV")
|
|
80
|
+
|
|
81
|
+
exec "$HERMES_BIN" "${gateway_args[@]}"
|
|
@@ -15,16 +15,29 @@ set -euo pipefail
|
|
|
15
15
|
|
|
16
16
|
ROLE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # agents/hermes/<role>
|
|
17
17
|
RUNTIME="$ROLE_DIR/runtime"
|
|
18
|
+
RUN_HOME="${HERMES_HOME:-$RUNTIME}"
|
|
18
19
|
PROMPT_FILE="$ROLE_DIR/.scripts/sentinel.prompt.md"
|
|
19
20
|
STATE_FILE="$RUNTIME/continuous-ticket-sentinel-state.json"
|
|
20
21
|
LOCK_FILE="$RUNTIME/continuous-ticket-sentinel.lock"
|
|
21
22
|
ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
23
|
+
FLEET_ENV="${HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
|
|
24
|
+
FLEET_ENV_LIBRARY="$ROLE_DIR/.scripts/lib/fleet-env.sh"
|
|
25
|
+
FLEET_ENV_PARSER="$ROLE_DIR/.scripts/lib/parse-fleet-env.py"
|
|
22
26
|
LOG_FILE="$RUNTIME/logs/heartbeat.log"
|
|
23
27
|
CHECKPOINT_BIN="$ROLE_DIR/.scripts/checkpoint.sh"
|
|
24
28
|
CHECKPOINT_STAMP="$RUNTIME/.last-checkpoint"
|
|
25
29
|
|
|
30
|
+
if [[ ! -f "$FLEET_ENV_LIBRARY" || -L "$FLEET_ENV_LIBRARY" \
|
|
31
|
+
|| ! -f "$FLEET_ENV_PARSER" || -L "$FLEET_ENV_PARSER" ]]; then
|
|
32
|
+
echo "heartbeat: trusted fleet environment loader unavailable" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
# shellcheck source=lib/fleet-env.sh
|
|
36
|
+
builtin source "$FLEET_ENV_LIBRARY"
|
|
37
|
+
load_fleet_environment "$FLEET_ENV" "$FLEET_ENV_PARSER"
|
|
38
|
+
|
|
26
39
|
# Hermes binary: explicit env > ~/.config/hermes-agent/hermes-bin > PATH.
|
|
27
|
-
HERMES_BIN="${HERMES_BIN:-}"
|
|
40
|
+
HERMES_BIN="${HERMES_BIN:-${HERMES_FLEET_BIN:-}}"
|
|
28
41
|
if [[ -z "$HERMES_BIN" ]]; then
|
|
29
42
|
if [[ -r "$HOME/.config/hermes-agent/hermes-bin" ]]; then
|
|
30
43
|
HERMES_BIN="$(cat "$HOME/.config/hermes-agent/hermes-bin")"
|
|
@@ -242,7 +255,7 @@ trap 'python3 "$ROLE_DIR/.scripts/momo-wip-lock.py" release "$WIP_LOCK" "hermes:
|
|
|
242
255
|
|
|
243
256
|
prompt="$(<"$PROMPT_FILE")"
|
|
244
257
|
set +e
|
|
245
|
-
env HERMES_HOME="$
|
|
258
|
+
env HERMES_HOME="$RUN_HOME" "$HERMES_BIN" chat -Q --source cron --max-turns 90 -q "$prompt"
|
|
246
259
|
status=$?
|
|
247
260
|
set -e
|
|
248
261
|
|