@delorenj/pjangler 1.2.21 → 1.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1852 -368
- package/dist/mcp-server.js +1782 -296
- package/package.json +14 -3
- package/templates/commonproject/copier.yml +3 -0
- package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
- package/templates/commonproject/template/mise.toml.jinja +9 -2
- package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
- package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
- package/templates/hermes-agent/README.md +70 -29
- package/templates/hermes-agent/copier.yml +9 -6
- package/templates/hermes-agent/docs/architecture.md +75 -51
- package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
- package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
- package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
- package/templates/hermes-agent/docs/operations.md +179 -81
- package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
- package/templates/hermes-agent/docs/sentinel/README.md +10 -10
- package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
- package/templates/hermes-agent/docs/sentinel/development.md +12 -15
- package/templates/hermes-agent/docs/sentinel.md +1 -1
- package/templates/hermes-agent/install-local.sh +41 -4
- package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
- package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
- package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
- package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
- package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
- package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
- package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
- package/templates/hermes-agent/template/.gitignore.jinja +5 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
- package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
- package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
- package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
- package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
- package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
- package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +1 -1
- package/templates/hermes-agent/template/SOUL.md.jinja +2 -2
- package/templates/hermes-agent/template/hermes.jinja +6 -3
- package/templates/hermes-agent/template/role.yaml.jinja +24 -13
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
- package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
- package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
- package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
- package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
- package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
- package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
- package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
- package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
- package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
- package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
- package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +0 -181
|
@@ -127,8 +127,8 @@ The script:
|
|
|
127
127
|
5. binds the PM to the existing board (it never creates one — it scrubs the
|
|
128
128
|
provider key from Copier's environment so `42-ticket-provider.sh` skips board
|
|
129
129
|
creation),
|
|
130
|
-
6. installs the PM's heartbeat timer
|
|
131
|
-
|
|
130
|
+
6. installs the PM's board-reconciliation heartbeat timer as a `launchd` agent
|
|
131
|
+
on macOS or a `systemd` timer on
|
|
132
132
|
Linux,
|
|
133
133
|
7. smoke-tests the board connection through the adapter.
|
|
134
134
|
|
|
@@ -160,7 +160,8 @@ order. Several of them reach outside the repository.
|
|
|
160
160
|
<!-- prettier-ignore -->
|
|
161
161
|
> [!CAUTION]
|
|
162
162
|
> Full provisioning is outward-facing and partly interactive.
|
|
163
|
-
> `20-runtime-repo.sh`
|
|
163
|
+
> `20-runtime-repo.sh` populates the ignored local runtime and links its profile;
|
|
164
|
+
> it creates no GitHub storage,
|
|
164
165
|
> `30-telegram.sh` prompts for a BotFather token and blocks waiting for input,
|
|
165
166
|
> and `42-ticket-provider.sh` can create a Plane project. Don't run full
|
|
166
167
|
> provisioning unattended.
|
|
@@ -171,10 +172,10 @@ script checks its flag and skips cleanly.
|
|
|
171
172
|
| Flag | Skips |
|
|
172
173
|
| --- | --- |
|
|
173
174
|
| `SKIP_TELEGRAM` | The interactive BotFather token step. |
|
|
174
|
-
| `SKIP_RUNTIME_REPO` |
|
|
175
|
+
| `SKIP_RUNTIME_REPO` | Legacy flag name: skip local runtime population/profile linking. No remote storage is created. |
|
|
175
176
|
| `SKIP_PLANE` | Creating a Plane project. |
|
|
176
|
-
| `SKIP_BLOODBANK` |
|
|
177
|
-
| `SKIP_SYSTEMD` | Installing `systemd` units (gateway
|
|
177
|
+
| `SKIP_BLOODBANK` | Compatibility no-op; Bloodbank ingress is fleet-shared. |
|
|
178
|
+
| `SKIP_SYSTEMD` | Installing `systemd` units (profile gateway and heartbeat timer). |
|
|
178
179
|
|
|
179
180
|
For example, a local install that creates no cloud resources:
|
|
180
181
|
|
|
@@ -191,9 +192,8 @@ on Linux through a `systemd` `EnvironmentFile` (for example
|
|
|
191
192
|
`~/.hermes/<agent_id>.env`); on macOS the `launchd` agent sources that same
|
|
192
193
|
per-agent env file, so write the key there.
|
|
193
194
|
|
|
194
|
-
`SKIP_SYSTEMD` gates the gateway
|
|
195
|
-
`70-systemd.sh`.
|
|
196
|
-
gated checkpoint, so a local install that wants the sentinel running must leave
|
|
195
|
+
`SKIP_SYSTEMD` gates the profile gateway and `heartbeat` timer in
|
|
196
|
+
`70-systemd.sh`. A local install that wants the sentinel running must leave
|
|
197
197
|
`SKIP_SYSTEMD` unset (or install the timer afterward).
|
|
198
198
|
|
|
199
199
|
## Propagating changes
|
|
@@ -219,12 +219,9 @@ The sentinel began as a bespoke loop on the `pm` role, was briefly extracted int
|
|
|
219
219
|
a standalone `scrum-master` role with its own `continuous-ticket-sentinel` timer,
|
|
220
220
|
and has now been folded back into the unified PM. Today there is exactly one
|
|
221
221
|
role (`pm`), one engine (under `.scripts/sentinel/`), and one timer
|
|
222
|
-
(`hermes-<agent>-heartbeat.timer`)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
`hermes-<agent>-checkpoint.timer` are gone; both are replaced by the single
|
|
226
|
-
`heartbeat` timer. The earlier standalone-role design is recoverable from Git
|
|
227
|
-
history if you ever need to compare.
|
|
222
|
+
(`hermes-<agent>-heartbeat.timer`) for board reconciliation. The earlier
|
|
223
|
+
standalone-role design is recoverable from Git history if you ever need to
|
|
224
|
+
compare.
|
|
228
225
|
|
|
229
226
|
## Known gotchas
|
|
230
227
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The PM runs a provider-agnostic ticket sentinel out-of-band on its heartbeat
|
|
4
4
|
timer: a board-reconciliation pass with an autonomous adversarial review (act,
|
|
5
|
-
do not wait)
|
|
5
|
+
do not wait). Pure-local runtime persistence is outside the heartbeat contract.
|
|
6
6
|
The full documentation lives in a dedicated handoff guide.
|
|
7
7
|
|
|
8
8
|
This page is kept as a stable pointer so existing links keep working.
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
# Provider creds: LINEAR_API_KEY | PLANE_API_KEY+PLANE_BASE | TRELLO_KEY+TRELLO_TOKEN
|
|
22
22
|
set -eu
|
|
23
23
|
|
|
24
|
+
# Fleet runtime publication. These values intentionally identify the reviewed
|
|
25
|
+
# fork commit; clean installs must never fall back to NousResearch/main.
|
|
26
|
+
HERMES_RUNTIME_GIT_URL="https://github.com/delorenj/hermes-agent.git"
|
|
27
|
+
HERMES_RUNTIME_GIT_REF="feature/PJAN-19-routing-publication"
|
|
28
|
+
HERMES_RUNTIME_GIT_SHA="113e1b182b6d72a7dd02a191f134a41668ceaf0e"
|
|
29
|
+
|
|
24
30
|
say() { printf '\033[36m%s\033[0m\n' "$*"; }
|
|
25
31
|
warn() { printf '\033[33m%s\033[0m\n' "$*" >&2; }
|
|
26
32
|
die() { printf '\033[31m%s\033[0m\n' "$*" >&2; exit 1; }
|
|
@@ -53,7 +59,38 @@ if command -v hermes >/dev/null 2>&1; then
|
|
|
53
59
|
say "1. hermes: found ($(command -v hermes))"
|
|
54
60
|
else
|
|
55
61
|
say "1. hermes: not found — installing"
|
|
56
|
-
|
|
62
|
+
HERMES_INSTALL_DIR="${HERMES_INSTALL_DIR:-$HOME/.hermes/hermes-agent}"
|
|
63
|
+
if [ "${HAT_DRY_RUN:-0}" = "1" ]; then
|
|
64
|
+
say " [dry-run] clone $HERMES_RUNTIME_GIT_URL@$HERMES_RUNTIME_GIT_REF"
|
|
65
|
+
say " [dry-run] verify and install commit $HERMES_RUNTIME_GIT_SHA"
|
|
66
|
+
else
|
|
67
|
+
command -v git >/dev/null 2>&1 || die "git is required to install the pinned Hermes runtime"
|
|
68
|
+
if [ -d "$HERMES_INSTALL_DIR/.git" ]; then
|
|
69
|
+
_origin="$(git -C "$HERMES_INSTALL_DIR" remote get-url origin 2>/dev/null || true)"
|
|
70
|
+
[ "$_origin" = "$HERMES_RUNTIME_GIT_URL" ] \
|
|
71
|
+
|| die "existing Hermes checkout origin is not the reviewed fleet fork: $HERMES_INSTALL_DIR"
|
|
72
|
+
git -C "$HERMES_INSTALL_DIR" fetch origin "$HERMES_RUNTIME_GIT_REF"
|
|
73
|
+
elif [ -e "$HERMES_INSTALL_DIR" ]; then
|
|
74
|
+
die "Hermes install path exists but is not the reviewed fork checkout: $HERMES_INSTALL_DIR"
|
|
75
|
+
else
|
|
76
|
+
mkdir -p "$(dirname "$HERMES_INSTALL_DIR")"
|
|
77
|
+
git clone --branch "$HERMES_RUNTIME_GIT_REF" --single-branch \
|
|
78
|
+
"$HERMES_RUNTIME_GIT_URL" "$HERMES_INSTALL_DIR"
|
|
79
|
+
fi
|
|
80
|
+
git -C "$HERMES_INSTALL_DIR" cat-file -e "$HERMES_RUNTIME_GIT_SHA^{commit}" \
|
|
81
|
+
|| die "pinned Hermes commit is unavailable from the reviewed fork"
|
|
82
|
+
git -C "$HERMES_INSTALL_DIR" merge-base --is-ancestor \
|
|
83
|
+
"$HERMES_RUNTIME_GIT_SHA" "origin/$HERMES_RUNTIME_GIT_REF" \
|
|
84
|
+
|| die "pinned Hermes commit is not on the reviewed publication ref"
|
|
85
|
+
bash "$HERMES_INSTALL_DIR/scripts/install.sh" \
|
|
86
|
+
--dir "$HERMES_INSTALL_DIR" \
|
|
87
|
+
--branch "$HERMES_RUNTIME_GIT_REF" \
|
|
88
|
+
--commit "$HERMES_RUNTIME_GIT_SHA" \
|
|
89
|
+
--skip-setup
|
|
90
|
+
_installed_sha="$(git -C "$HERMES_INSTALL_DIR" rev-parse HEAD)"
|
|
91
|
+
[ "$_installed_sha" = "$HERMES_RUNTIME_GIT_SHA" ] \
|
|
92
|
+
|| die "Hermes installer did not retain pinned commit $HERMES_RUNTIME_GIT_SHA"
|
|
93
|
+
fi
|
|
57
94
|
command -v hermes >/dev/null 2>&1 || die "hermes install did not put 'hermes' on PATH. Open a new shell and re-run."
|
|
58
95
|
fi
|
|
59
96
|
HERMES_BIN="$(command -v hermes)"
|
|
@@ -82,6 +119,9 @@ else
|
|
|
82
119
|
[fleet]
|
|
83
120
|
hermes_bin = "$HERMES_BIN"
|
|
84
121
|
hermes_repo = "$HOME/.hermes/hermes-agent"
|
|
122
|
+
hermes_git_url = "$HERMES_RUNTIME_GIT_URL"
|
|
123
|
+
hermes_git_ref = "$HERMES_RUNTIME_GIT_REF"
|
|
124
|
+
hermes_git_sha = "$HERMES_RUNTIME_GIT_SHA"
|
|
85
125
|
fleet_env = "~/.hermes/fleet.env"
|
|
86
126
|
registry_file = "~/.hermes/agents-registry.yaml"
|
|
87
127
|
|
|
@@ -92,9 +132,6 @@ runtime_repo_owner = ""
|
|
|
92
132
|
base = "${PLANE_BASE:-}"
|
|
93
133
|
workspace = ""
|
|
94
134
|
|
|
95
|
-
[bloodbank]
|
|
96
|
-
nats_host = "127.0.0.1"
|
|
97
|
-
nats_port = 4222
|
|
98
135
|
TOML
|
|
99
136
|
fi
|
|
100
137
|
fi
|
|
@@ -13,13 +13,15 @@ and recoverable on failure.
|
|
|
13
13
|
| `memories/MEMORY.md` | yes | The condensed mental-model summary loaded each session |
|
|
14
14
|
| `memories/USER.md` | yes | The operator's persona (Jarad DeLorenzo, ...) |
|
|
15
15
|
| `sessions/sessions.db` | yes (LFS) | SQLite store of every conversation |
|
|
16
|
-
| `bloodbank-consumer.py` | yes | NATS subscriber using canonical subjects plus envelope routing data |
|
|
17
16
|
| `decisions/` | yes | Agent-emitted decisions, one file per important call |
|
|
18
17
|
| `.env` | **no** | API keys + Telegram bot token (per-machine secret) |
|
|
19
18
|
| `auth.json` | **no** | Deprecated local OAuth store; fleet auth defaults to `HERMES_OAUTH_FILE=~/.hermes/auth.json` |
|
|
20
19
|
| `audio_cache/`, `image_cache/` | **no** | Regenerable caches |
|
|
21
20
|
| `sandboxes/` | **no** | Per-session ephemeral execution dirs |
|
|
22
|
-
|
|
21
|
+
|
|
22
|
+
Bloodbank ingress is fleet-shared. The fleet gateway discovers this profile
|
|
23
|
+
through `~/.hermes/agents-registry.yaml` and routes commands by
|
|
24
|
+
`data.target_agent_id`; this runtime has no consumer process or inbox bridge.
|
|
23
25
|
|
|
24
26
|
## Checkpoint cadence
|
|
25
27
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# degit-runtime — make a repo's Hermes agent runtime(s) PURE LOCAL state.
|
|
3
|
+
#
|
|
4
|
+
# Run from (or point --root at) the ROOT of a project repo. Discovers every
|
|
5
|
+
# agents/hermes/*/runtime under it and, in one shot, removes it from git and
|
|
6
|
+
# gitignores it so it never re-enters version control.
|
|
7
|
+
#
|
|
8
|
+
# Policy = pure-local (fleet decision D2, 2026-07-21): the runtime is no longer
|
|
9
|
+
# a git-tracked submodule NOR its own checkpoint repo. Per-agent memory
|
|
10
|
+
# durability moves to the per-repo Hindsight bank. The
|
|
11
|
+
# github.com/delorenj/agent-hm-*-pm remotes are left intact (recoverable) but
|
|
12
|
+
# are no longer pushed to. This is the INVERSE of repair-runtime-checkpoint.sh,
|
|
13
|
+
# which keeps the checkpoint repo alive.
|
|
14
|
+
#
|
|
15
|
+
# Per runtime:
|
|
16
|
+
# Layer A (project repo, if one exists):
|
|
17
|
+
# - gitlink submodule -> git rm --cached <rel>
|
|
18
|
+
# + drop [submodule "<rel>"] from .gitmodules
|
|
19
|
+
# + rm -rf .git/modules/<rel>
|
|
20
|
+
# - plain tracked files -> git rm -r --cached <rel>
|
|
21
|
+
# - ensure .gitignore ignores agents/hermes/*/runtime/
|
|
22
|
+
# Layer B (the runtime itself):
|
|
23
|
+
# - if it has unpushed commits, push once (best-effort) unless --force
|
|
24
|
+
# - rm -rf <runtime>/.git (retire the checkpoint repo)
|
|
25
|
+
# - write <runtime>/.gitignore = "*\n!.gitignore" (pure-local marker)
|
|
26
|
+
#
|
|
27
|
+
# REPORTED but not touched here (fleet-level; handled by fleet-prune-debris.sh):
|
|
28
|
+
# ~/.hermes/profiles/<profile> symlink, agents-registry.yaml runtime_repo,
|
|
29
|
+
# and the now-dead -checkpoint.service/.timer units.
|
|
30
|
+
#
|
|
31
|
+
# Dry-run by default (prints the plan). --apply mutates. Idempotent: every step
|
|
32
|
+
# checks state first, so a second run is a clean no-op.
|
|
33
|
+
#
|
|
34
|
+
# Usage: degit-runtime.sh [--apply] [--force] [--root DIR]
|
|
35
|
+
set -euo pipefail
|
|
36
|
+
|
|
37
|
+
APPLY=0; FORCE=0; ROOT=""
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--apply) APPLY=1 ;;
|
|
41
|
+
--force) FORCE=1 ;;
|
|
42
|
+
--root) shift; ROOT="${1:-}" ;;
|
|
43
|
+
-h|--help) sed -n '2,37p' "$0"; exit 0 ;;
|
|
44
|
+
*) printf 'degit-runtime: unknown arg: %s\n' "$1" >&2; exit 2 ;;
|
|
45
|
+
esac
|
|
46
|
+
shift
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
|
50
|
+
note(){ printf ' %s\n' "$*"; }
|
|
51
|
+
# run CMD... — execute on --apply, otherwise print the plan line.
|
|
52
|
+
run(){ if [ "$APPLY" -eq 1 ]; then "$@"; else printf ' [would] %s\n' "$*"; fi; }
|
|
53
|
+
|
|
54
|
+
ROOT="${ROOT:-$PWD}"
|
|
55
|
+
[ -d "$ROOT" ] || die "no such directory: $ROOT"
|
|
56
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
57
|
+
[ -d "$ROOT/agents/hermes" ] || die "no agents/hermes under $ROOT — run from a project repo root (or pass --root)"
|
|
58
|
+
|
|
59
|
+
MODE="dry-run"; [ "$APPLY" -eq 1 ] && MODE="APPLY"
|
|
60
|
+
printf '== degit-runtime (%s) :: %s ==\n' "$MODE" "$ROOT"
|
|
61
|
+
|
|
62
|
+
# Project repo root (the superproject). Empty when the project has no git at all
|
|
63
|
+
# (e.g. DeLoDocs, pjangler) — Layer A is then skipped.
|
|
64
|
+
PROJ="$(git -C "$ROOT" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
65
|
+
if [ -n "$PROJ" ]; then note "project repo: $PROJ"; else note "project repo: (none — Layer A skipped)"; fi
|
|
66
|
+
|
|
67
|
+
# Discover runtimes: agents/hermes/<role>/runtime (depth 2 under agents/hermes).
|
|
68
|
+
mapfile -t RUNTIMES < <(find "$ROOT/agents/hermes" -mindepth 2 -maxdepth 2 -type d -name runtime 2>/dev/null | sort)
|
|
69
|
+
[ "${#RUNTIMES[@]}" -gt 0 ] || die "no agents/hermes/*/runtime found under $ROOT"
|
|
70
|
+
note "runtimes: ${#RUNTIMES[@]}"
|
|
71
|
+
|
|
72
|
+
CHANGED=0
|
|
73
|
+
|
|
74
|
+
ensure_project_ignore(){ # $1 = relpath of runtime from project root
|
|
75
|
+
local rel="$1" gi="$PROJ/.gitignore"
|
|
76
|
+
# Prefer a broad, role-agnostic rule; fall back to the exact path.
|
|
77
|
+
if git -C "$PROJ" check-ignore -q "$rel" 2>/dev/null; then
|
|
78
|
+
note ".gitignore already ignores $rel"; return 0
|
|
79
|
+
fi
|
|
80
|
+
local rule="agents/hermes/*/runtime/"
|
|
81
|
+
if [ -f "$gi" ] && grep -qxF "$rule" "$gi" 2>/dev/null; then
|
|
82
|
+
note ".gitignore already has rule: $rule"; return 0
|
|
83
|
+
fi
|
|
84
|
+
if [ "$APPLY" -eq 1 ]; then
|
|
85
|
+
printf '\n# Hermes agent runtime — pure-local state, never tracked (degit-runtime).\n%s\n' "$rule" >> "$gi"
|
|
86
|
+
note "appended ignore rule to .gitignore: $rule"
|
|
87
|
+
else
|
|
88
|
+
printf ' [would] append %s to %s\n' "$rule" "$gi"
|
|
89
|
+
fi
|
|
90
|
+
CHANGED=1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
layer_a(){ # $1 = absolute runtime dir
|
|
94
|
+
local rt="$1" rel
|
|
95
|
+
[ -n "$PROJ" ] || return 0
|
|
96
|
+
rel="${rt#"$PROJ"/}"
|
|
97
|
+
# Is it a gitlink submodule (mode 160000) in the index?
|
|
98
|
+
if git -C "$PROJ" ls-files --stage -- "$rel" 2>/dev/null | grep -q '^160000 '; then
|
|
99
|
+
note "Layer A: '$rel' is a gitlink submodule -> de-submodule"
|
|
100
|
+
# -f: the gitlink may be staged with a pointer differing from HEAD (e.g. a
|
|
101
|
+
# freshly-added, not-yet-committed submodule). --cached is index-only and
|
|
102
|
+
# never touches the working tree, so forcing is safe.
|
|
103
|
+
run git -C "$PROJ" rm --cached -f -q -- "$rel"
|
|
104
|
+
if [ -f "$PROJ/.gitmodules" ] && git -C "$PROJ" config -f "$PROJ/.gitmodules" --get "submodule.$rel.url" >/dev/null 2>&1; then
|
|
105
|
+
run git -C "$PROJ" config -f "$PROJ/.gitmodules" --remove-section "submodule.$rel"
|
|
106
|
+
# Drop .gitmodules entirely once no [submodule] sections remain.
|
|
107
|
+
# (In dry-run the section is still present, so this stays conservative.)
|
|
108
|
+
if [ ! -f "$PROJ/.gitmodules" ] || ! grep -q '^\[submodule' "$PROJ/.gitmodules" 2>/dev/null; then
|
|
109
|
+
run rm -f "$PROJ/.gitmodules"
|
|
110
|
+
run git -C "$PROJ" rm --cached -q --ignore-unmatch -- .gitmodules
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
# The real gitdir (runtime layer already removed the runtime/.git pointer).
|
|
114
|
+
[ -e "$PROJ/.git/modules/$rel" ] && run rm -rf "$PROJ/.git/modules/$rel"
|
|
115
|
+
CHANGED=1
|
|
116
|
+
elif git -C "$PROJ" ls-files --error-unmatch -- "$rel" >/dev/null 2>&1; then
|
|
117
|
+
note "Layer A: '$rel' is tracked as plain content -> unstage"
|
|
118
|
+
run git -C "$PROJ" rm -r --cached -q -- "$rel"
|
|
119
|
+
CHANGED=1
|
|
120
|
+
else
|
|
121
|
+
note "Layer A: '$rel' is not tracked in the project repo (good)"
|
|
122
|
+
fi
|
|
123
|
+
ensure_project_ignore "$rel"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
layer_b(){ # $1 = absolute runtime dir
|
|
127
|
+
local rt="$1"
|
|
128
|
+
# hard guard before any rm -rf
|
|
129
|
+
case "$rt" in */runtime) : ;; *) die "refusing: '$rt' does not end in /runtime" ;; esac
|
|
130
|
+
[ -d "$rt" ] || die "refusing: runtime dir vanished: $rt"
|
|
131
|
+
|
|
132
|
+
if [ -e "$rt/.git" ]; then
|
|
133
|
+
# Guard unpushed local commits: push once (best-effort) unless --force.
|
|
134
|
+
local ahead=0
|
|
135
|
+
if git -C "$rt" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
|
136
|
+
ahead="$(git -C "$rt" rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)"
|
|
137
|
+
else
|
|
138
|
+
ahead="unknown(no-upstream)"
|
|
139
|
+
fi
|
|
140
|
+
note "Layer B: runtime has .git (unpushed ahead: $ahead)"
|
|
141
|
+
if [ "$ahead" != "0" ]; then
|
|
142
|
+
if [ "$FORCE" -eq 1 ]; then
|
|
143
|
+
note " --force: discarding local .git without pushing ($ahead ahead)"
|
|
144
|
+
else
|
|
145
|
+
note " attempting one final push before retiring .git (--force to skip)"
|
|
146
|
+
if [ "$APPLY" -eq 1 ]; then
|
|
147
|
+
if GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=20' \
|
|
148
|
+
timeout 45 git -C "$rt" push 2>/dev/null; then
|
|
149
|
+
note " final push OK"
|
|
150
|
+
else
|
|
151
|
+
note " MANUAL: final push FAILED and commits are unpushed — skipping .git removal for this runtime (re-run with --force to discard, or push by hand)"
|
|
152
|
+
return 0
|
|
153
|
+
fi
|
|
154
|
+
else
|
|
155
|
+
printf ' [would] git -C %s push (final backup)\n' "$rt"
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
run rm -rf "$rt/.git"
|
|
160
|
+
CHANGED=1
|
|
161
|
+
else
|
|
162
|
+
note "Layer B: runtime already has no .git (good)"
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# Pure-local marker: ignore everything except the marker itself.
|
|
166
|
+
local gi="$rt/.gitignore" want=$'*\n!.gitignore'
|
|
167
|
+
if [ -f "$gi" ] && [ "$(cat "$gi" 2>/dev/null)" = "$want" ]; then
|
|
168
|
+
note "Layer B: runtime/.gitignore already pure-local"
|
|
169
|
+
else
|
|
170
|
+
if [ "$APPLY" -eq 1 ]; then printf '%s\n' "$want" > "$gi"; note "wrote pure-local runtime/.gitignore"
|
|
171
|
+
else printf ' [would] write pure-local %s\n' "$gi"; fi
|
|
172
|
+
CHANGED=1
|
|
173
|
+
fi
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for rt in "${RUNTIMES[@]}"; do
|
|
177
|
+
printf -- '-- %s\n' "$rt"
|
|
178
|
+
# Runtime layer FIRST: any final push needs the gitdir, which the project
|
|
179
|
+
# layer's `rm -rf .git/modules/<rel>` would otherwise remove.
|
|
180
|
+
layer_b "$rt"
|
|
181
|
+
layer_a "$rt"
|
|
182
|
+
done
|
|
183
|
+
|
|
184
|
+
echo
|
|
185
|
+
if [ "$CHANGED" -eq 0 ]; then
|
|
186
|
+
printf 'RESULT: already pure-local — nothing to do.\n'
|
|
187
|
+
elif [ "$APPLY" -eq 1 ]; then
|
|
188
|
+
printf 'RESULT: applied. Commit the project-repo changes (.gitignore / .gitmodules / unstaged runtime) when ready.\n'
|
|
189
|
+
printf ' Fleet-level cleanup (dead -checkpoint units, profile symlink) -> fleet-prune-debris.sh\n'
|
|
190
|
+
else
|
|
191
|
+
printf 'RESULT: dry-run only. Re-run with --apply to mutate.\n'
|
|
192
|
+
fi
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fleet-prune-debris — remove evolutionary debris from the Hermes fleet's
|
|
3
|
+
# systemd USER units, and retire now-dead runtime-checkpoint units for agents
|
|
4
|
+
# whose runtime is already pure-local (see degit-runtime.sh, fleet decision D2).
|
|
5
|
+
#
|
|
6
|
+
# Conservative by design. It only AUTO-removes things that are unambiguously
|
|
7
|
+
# dead:
|
|
8
|
+
# - a hardcoded allowlist of DISABLED stale units (old singleton gateways,
|
|
9
|
+
# the delodocs per-agent dashboard) — and only while they are NOT active;
|
|
10
|
+
# - *.bak-* backup copies of hermes unit files;
|
|
11
|
+
# - hermes-<id>-pm-checkpoint.{service,timer} whose runtime has NO .git
|
|
12
|
+
# (already pure-local -> the checkpoint unit can never do anything again).
|
|
13
|
+
# Everything ambiguous (active/enabled redundant units, the carrie persona, the
|
|
14
|
+
# delonet-company-reporter archetype, FAILED units still git-backed) is REPORTED
|
|
15
|
+
# as MANUAL and never touched.
|
|
16
|
+
#
|
|
17
|
+
# Dry-run by default. --apply performs the removals + one daemon-reload.
|
|
18
|
+
#
|
|
19
|
+
# Usage: fleet-prune-debris.sh [--apply]
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
APPLY=0
|
|
23
|
+
for a in "$@"; do
|
|
24
|
+
case "$a" in
|
|
25
|
+
--apply) APPLY=1 ;;
|
|
26
|
+
-h|--help) sed -n '2,26p' "$0"; exit 0 ;;
|
|
27
|
+
*) printf 'fleet-prune-debris: unknown arg: %s\n' "$a" >&2; exit 2 ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
SD="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
32
|
+
[ -d "$SD" ] || { printf 'ERROR: no systemd user dir at %s\n' "$SD" >&2; exit 1; }
|
|
33
|
+
|
|
34
|
+
MODE="dry-run"; [ "$APPLY" -eq 1 ] && MODE="APPLY"
|
|
35
|
+
printf '== fleet-prune-debris (%s) :: %s ==\n' "$MODE" "$SD"
|
|
36
|
+
note(){ printf ' %s\n' "$*"; }
|
|
37
|
+
uc(){ systemctl --user "$@" 2>/dev/null; } # user systemctl, quiet
|
|
38
|
+
is_active(){ uc is-active "$1" >/dev/null; }
|
|
39
|
+
exists(){ [ -e "$SD/$1" ]; }
|
|
40
|
+
DID=0
|
|
41
|
+
|
|
42
|
+
# remove_unit BASENAME... — disable+stop (if loaded) then rm the file(s).
|
|
43
|
+
remove_unit(){
|
|
44
|
+
local u
|
|
45
|
+
for u in "$@"; do
|
|
46
|
+
exists "$u" || { note "absent (ok): $u"; continue; }
|
|
47
|
+
if [ "$APPLY" -eq 1 ]; then
|
|
48
|
+
uc disable --now "$u" || true
|
|
49
|
+
rm -f "$SD/$u"
|
|
50
|
+
note "removed: $u"
|
|
51
|
+
else
|
|
52
|
+
note "[would] disable --now + rm: $u"
|
|
53
|
+
fi
|
|
54
|
+
DID=1
|
|
55
|
+
done
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
printf '\n[1] disabled stale units (old singleton gateways / stale dashboard)\n'
|
|
59
|
+
# From recon: these are DISABLED + superseded. Auto-remove only if not active.
|
|
60
|
+
for u in hermes-gateway.service hermes-gateway-de01182b.service hermes-dashboard-delodocs-pm.service; do
|
|
61
|
+
if exists "$u"; then
|
|
62
|
+
if is_active "$u"; then note "MANUAL: $u is ACTIVE — expected disabled; confirm before removing";
|
|
63
|
+
else remove_unit "$u"; fi
|
|
64
|
+
else note "absent (ok): $u"; fi
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
printf '\n[2] *.bak-* backup copies of hermes unit files\n'
|
|
68
|
+
shopt -s nullglob
|
|
69
|
+
BAKS=("$SD"/hermes-*.bak-*) # matches *.service.bak-* / *.timer.bak-* too (glob * spans dots)
|
|
70
|
+
shopt -u nullglob
|
|
71
|
+
if [ "${#BAKS[@]}" -eq 0 ]; then note "none"; else
|
|
72
|
+
for f in "${BAKS[@]}"; do
|
|
73
|
+
if [ "$APPLY" -eq 1 ]; then rm -f "$f"; note "removed: $(basename "$f")"; else note "[would] rm: $(basename "$f")"; fi
|
|
74
|
+
DID=1
|
|
75
|
+
done
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
printf '\n[3] dead checkpoint units for pure-local runtimes\n'
|
|
79
|
+
shopt -s nullglob
|
|
80
|
+
CKPTS=("$SD"/hermes-*-pm-checkpoint.service)
|
|
81
|
+
shopt -u nullglob
|
|
82
|
+
if [ "${#CKPTS[@]}" -eq 0 ]; then note "none"; else
|
|
83
|
+
for svc in "${CKPTS[@]}"; do
|
|
84
|
+
base="$(basename "$svc")"
|
|
85
|
+
# Derive the runtime from ExecStart=<role_dir>/.scripts/checkpoint.sh
|
|
86
|
+
exec_line="$(grep -m1 '^ExecStart=' "$svc" 2>/dev/null | sed 's/^ExecStart=//')"
|
|
87
|
+
script="${exec_line%% *}" # strip any args
|
|
88
|
+
role_dir="$(dirname "$(dirname "$script")")" # .../pm/.scripts/x.sh -> .../pm
|
|
89
|
+
runtime="$role_dir/runtime"
|
|
90
|
+
if [ ! -d "$runtime" ]; then
|
|
91
|
+
note "MANUAL: $base -> runtime not found ($runtime); leaving"
|
|
92
|
+
continue
|
|
93
|
+
fi
|
|
94
|
+
if [ -e "$runtime/.git" ]; then
|
|
95
|
+
note "SKIP: $base -> runtime still git-backed (degit-runtime.sh first): $runtime"
|
|
96
|
+
else
|
|
97
|
+
note "pure-local -> retiring: $base (+ .timer)"
|
|
98
|
+
remove_unit "$base" "${base%.service}.timer"
|
|
99
|
+
fi
|
|
100
|
+
done
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
printf '\n[4] MANUAL review (not auto-touched)\n'
|
|
104
|
+
# Running-redundant / ambiguous units surfaced for a human decision.
|
|
105
|
+
for u in hermes-gateway-intelliforia-voice-agent.service hermes-carrie-backend.service hermes-carrie-telegram-gateway.service; do
|
|
106
|
+
exists "$u" && note "MANUAL: $u ($(uc is-active "$u" || true)/$(uc is-enabled "$u" || true)) — redundant/legacy persona; decide keep vs retire"
|
|
107
|
+
done
|
|
108
|
+
# delonet-company-reporter is a different archetype (not a PM).
|
|
109
|
+
shopt -s nullglob
|
|
110
|
+
for u in "$SD"/hermes-delonet-company-reporter-*.service; do
|
|
111
|
+
b="$(basename "$u")"; note "MANUAL: $b ($(uc is-active "$b" || true)) — non-PM reporter archetype; repair or remove as a unit"
|
|
112
|
+
done
|
|
113
|
+
shopt -u nullglob
|
|
114
|
+
# Any remaining failed hermes units.
|
|
115
|
+
FAILED="$(uc --plain --no-legend list-units --state=failed 'hermes-*' | awk '{print $1}')"
|
|
116
|
+
if [ -n "$FAILED" ]; then
|
|
117
|
+
printf '%s\n' "$FAILED" | while read -r f; do [ -n "$f" ] && note "FAILED: $f — investigate (degit fixes git-push failures; missing-script units are orphans to remove)"; done
|
|
118
|
+
else note "no failed hermes units"; fi
|
|
119
|
+
|
|
120
|
+
echo
|
|
121
|
+
if [ "$APPLY" -eq 1 ]; then
|
|
122
|
+
uc daemon-reload || true
|
|
123
|
+
printf 'RESULT: applied + daemon-reload. Re-run to confirm idempotent (should be all "absent/none/SKIP").\n'
|
|
124
|
+
elif [ "$DID" -eq 1 ]; then
|
|
125
|
+
printf 'RESULT: dry-run. Re-run with --apply to remove the [would] items above.\n'
|
|
126
|
+
else
|
|
127
|
+
printf 'RESULT: nothing to prune (already clean) — only MANUAL items remain.\n'
|
|
128
|
+
fi
|