@delorenj/pjangler 1.2.19 → 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 +2353 -518
- package/dist/mcp-server.js +2331 -494
- package/package.json +14 -3
- package/templates/commonproject/copier.yml +6 -4
- package/templates/commonproject/template/.agents/hooks/README.md +13 -26
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
- package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
- package/templates/commonproject/template/.agents/local.example.json +2 -8
- package/templates/commonproject/template/.agents/skills.json +6 -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 +16 -16
- 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 +12 -1
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +21 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +36 -15
- 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/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
- 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
|
@@ -1,171 +1,31 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Status:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
## Root cause
|
|
33
|
-
|
|
34
|
-
`agents/hermes/<role>/runtime` is registered as a **git submodule** in the
|
|
35
|
-
project repo (`.gitmodules` has its URL; the parent index holds a `160000`
|
|
36
|
-
gitlink), **but the submodule is not populated** — there is no `runtime/.git`,
|
|
37
|
-
and `.git/modules/<path>` is gone. So any git command run *inside* `runtime/`
|
|
38
|
-
resolves **up to the parent repo**, which treats `runtime/` as an unpopulated
|
|
39
|
-
submodule path and makes `git add -A` fatal (exit 128).
|
|
40
|
-
|
|
41
|
-
Confirm it:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
RT=agents/hermes/<role>/runtime
|
|
45
|
-
git -C "$RT" rev-parse --show-toplevel # prints the PARENT repo, not RT -> red flag
|
|
46
|
-
ls "$RT/.git" # missing -> unpopulated
|
|
47
|
-
git ls-files -s "$RT" | head -1 # mode 160000 <sha> ... -> gitlink
|
|
48
|
-
git config -f .gitmodules --get-regexp "$RT" # submodule.<path>.url git@...
|
|
49
|
-
ls .git/modules/"$RT" 2>/dev/null || echo "objects gone -> must re-fetch from remote"
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Consequence: the agent's durable brain (`SOUL.md`, `memories/`, `profiles/`,
|
|
53
|
-
`skills/`, config) lives on disk (the running agent still reads it) but is **not
|
|
54
|
-
committed or pushed anywhere** — no backup until this is repaired.
|
|
55
|
-
|
|
56
|
-
## The fix (safe, in-place, preserves all on-disk state)
|
|
57
|
-
|
|
58
|
-
The key safety property: `git reset --mixed` moves `HEAD` + the index only and
|
|
59
|
-
**never rewrites working-tree files**, so the live brain (and a multi-hundred-MB
|
|
60
|
-
`state.db`) is untouched. Never use `git submodule update`/`git checkout <tree>`
|
|
61
|
-
here — those would overwrite live, uncommitted edits with the older committed
|
|
62
|
-
versions.
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
RT=agents/hermes/<role>/runtime
|
|
66
|
-
URL=$(git config -f .gitmodules --get submodule."$RT".url)
|
|
67
|
-
|
|
68
|
-
# 0. remote must be reachable (read-only)
|
|
69
|
-
GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=15' git ls-remote "$URL" >/dev/null
|
|
70
|
-
|
|
71
|
-
cd "$RT"
|
|
72
|
-
# 1. re-init git in place on branch 'main' (creates .git/ only; no file changes)
|
|
73
|
-
git init -q
|
|
74
|
-
git symbolic-ref HEAD refs/heads/main
|
|
75
|
-
git remote add origin "$URL"
|
|
76
|
-
git fetch -q origin
|
|
77
|
-
# 2. re-base history onto the remote tip WITHOUT touching the working tree
|
|
78
|
-
git reset --mixed -q origin/main
|
|
79
|
-
git branch --set-upstream-to=origin/main main 2>/dev/null || true
|
|
80
|
-
# 3. the repo uses git-lfs for *.db/images (.gitattributes); register filters
|
|
81
|
-
git lfs install --local
|
|
82
|
-
# 4. restore tracked config that may be missing on disk (avoids spurious deletes)
|
|
83
|
-
git checkout origin/main -- .gitattributes README.md 2>/dev/null || true
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Make it sustainable: back up the brain, not volatile state
|
|
87
|
-
|
|
88
|
-
Even though `.gitattributes` routes `*.db` through LFS, committing a live,
|
|
89
|
-
constantly-growing `state.db` (often hundreds of MB) every hour is not
|
|
90
|
-
sustainable, and `lsp/` (node_modules), `checkpoints/` (a nested object store),
|
|
91
|
-
and `sessions/` (ephemeral dumps) must never be committed. The checkpoint repo's
|
|
92
|
-
job is the **durable brain**. Ensure `runtime/.gitignore` contains:
|
|
93
|
-
|
|
94
|
-
```gitignore
|
|
95
|
-
# Runtime working state — NOT part of the durable brain backup.
|
|
96
|
-
*.db
|
|
97
|
-
*.db-wal
|
|
98
|
-
*.db-shm
|
|
99
|
-
*.sqlite
|
|
100
|
-
*.sqlite3
|
|
101
|
-
*.lock
|
|
102
|
-
.update_check
|
|
103
|
-
lsp/
|
|
104
|
-
checkpoints/
|
|
105
|
-
sessions/
|
|
106
|
-
cron/
|
|
107
|
-
*_cache.json
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Then untrack anything volatile that is currently tracked (keeps the files on
|
|
111
|
-
disk):
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
git rm -r --cached --ignore-unmatch state.db state.db-wal state.db-shm cron
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Verify BEFORE committing (mandatory safety gate)
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
git add -A --dry-run | awk '{print $2}' | while read -r f; do
|
|
121
|
-
[ -f "$f" ] && s=$(stat -c%s "$f") && [ "$s" -gt 1048576 ] && echo "BIG: $((s/1048576))MB $f"; done
|
|
122
|
-
# ^ must print nothing. Also confirm no state.db / lsp/ / sessions/ / checkpoints/,
|
|
123
|
-
# and no secrets (.env, auth.json, *.key, *token*) in the staged set.
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Commit, push, and bring the service green
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
bash agents/hermes/<role>/.scripts/checkpoint.sh # exits 0, pushes brain
|
|
130
|
-
systemctl --user reset-failed hermes-<agent>-heartbeat.service
|
|
131
|
-
systemctl --user start hermes-<agent>-heartbeat.service
|
|
132
|
-
systemctl --user show hermes-<agent>-heartbeat.service -p Result -p ExecMainStatus
|
|
133
|
-
# Result=success ExecMainStatus=0
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## One-shot
|
|
137
|
-
|
|
138
|
-
`scripts/repair-runtime-checkpoint.sh` automates all of the above. It is
|
|
139
|
-
**dry-run by default** (diagnoses and prints the plan) and only mutates with
|
|
140
|
-
`--apply`. Run it from a project repo root:
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
# diagnose
|
|
144
|
-
hermes-agent-template/scripts/repair-runtime-checkpoint.sh agents/hermes/pm/runtime
|
|
145
|
-
# apply the in-place re-attach + .gitignore policy + untrack (no commit)
|
|
146
|
-
hermes-agent-template/scripts/repair-runtime-checkpoint.sh --apply agents/hermes/pm/runtime
|
|
147
|
-
# then validate the service
|
|
148
|
-
systemctl --user reset-failed <svc> && systemctl --user start <svc>
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## Fleet sweep — this is rarely just one repo
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
systemctl --user list-units --all '*-heartbeat.service' | grep -i failed
|
|
155
|
-
# for each failing agent, locate its runtime and run the repair script.
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Lessons learned (why this happened / what to remember)
|
|
159
|
-
|
|
160
|
-
- An **unpopulated submodule** silently redirects in-directory git commands to
|
|
161
|
-
the parent repo; `git add -A` then fatals with exit 128. The fix is to restore
|
|
162
|
-
the submodule's own `.git` (objects from the remote), not to touch the parent.
|
|
163
|
-
- `git reset --mixed <ref>` is the safe way to re-attach history to a populated
|
|
164
|
-
working directory: it preserves every on-disk file.
|
|
165
|
-
- The checkpoint must **exclude volatile working state** (`state.db*`, `lsp/`,
|
|
166
|
-
`checkpoints/`, `sessions/`, caches, locks). Back up identity + memory + skills.
|
|
167
|
-
- Core dumps from the agent will **not** appear in `coredumpctl`/`/var/crash` on
|
|
168
|
-
Ubuntu: apport discards crashes from unpackaged venv-Python binaries. Use
|
|
169
|
-
`PYTHONFAULTHANDLER=1` or a private `core_pattern` to capture a backtrace.
|
|
170
|
-
- Always run the fleet sweep — provisioning copies this layout to every agent,
|
|
171
|
-
so a structural fault tends to be fleet-wide.
|
|
1
|
+
# Historical runbook: retired checkpoint architecture
|
|
2
|
+
|
|
3
|
+
Status: Historical evidence only — **do not execute**
|
|
4
|
+
|
|
5
|
+
This page formerly described recovery for an older per-agent persistence
|
|
6
|
+
design. That design is not the current Hermes template contract, and its
|
|
7
|
+
commands have intentionally been removed so an operator cannot mistake them
|
|
8
|
+
for a supported repair procedure.
|
|
9
|
+
|
|
10
|
+
Current agents use an ignored, pure-local
|
|
11
|
+
`agents/hermes/<role>/runtime/` directory. Provisioning refuses stale project
|
|
12
|
+
gitlinks or mappings at that exact path and preserves all existing runtime
|
|
13
|
+
bytes. The heartbeat performs board reconciliation; it is not the runtime
|
|
14
|
+
backup mechanism.
|
|
15
|
+
|
|
16
|
+
For current recovery and retirement procedures, use
|
|
17
|
+
[Operations](../operations.md#back-up-and-restore-an-agent). In particular:
|
|
18
|
+
|
|
19
|
+
- configure and verify an encrypted filesystem backup for the exact runtime
|
|
20
|
+
path;
|
|
21
|
+
- treat Hindsight as recovery for only the memories/events previously written
|
|
22
|
+
to its bank;
|
|
23
|
+
- treat the secret manager as recovery for only credentials deliberately
|
|
24
|
+
stored there;
|
|
25
|
+
- retire services and profile links without removing runtime data; and
|
|
26
|
+
- preserve retired runtime data because this release intentionally ships no
|
|
27
|
+
automated purge; any future purge requires a separately reviewed path-safe
|
|
28
|
+
tool and a verified off-host backup.
|
|
29
|
+
|
|
30
|
+
The earlier procedure remains recoverable from repository history for incident
|
|
31
|
+
forensics. Repository history is evidence, not an active operator interface.
|
|
@@ -20,9 +20,9 @@ and emits a decision event. A real finding is `held` and the ticket goes back to
|
|
|
20
20
|
active.
|
|
21
21
|
|
|
22
22
|
There is no longer a separate `scrum-master` role; the sentinel folded into the
|
|
23
|
-
PM and now runs as
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
PM and now runs as the PM's **heartbeat** systemd-timer tick
|
|
24
|
+
(`.scripts/heartbeat.sh`). Under the pure-local runtime contract this timer
|
|
25
|
+
performs board reconciliation only. The engine itself lives under the PM at
|
|
26
26
|
`.scripts/sentinel/`.
|
|
27
27
|
|
|
28
28
|
It talks to the ticket board through a pluggable adapter, so the same engine
|
|
@@ -50,8 +50,8 @@ The following is true as of June 1, 2026.
|
|
|
50
50
|
|
|
51
51
|
## Quick local install
|
|
52
52
|
|
|
53
|
-
For a local, single-machine install (no
|
|
54
|
-
|
|
53
|
+
For a local, single-machine install (no Telegram and no NATS), use the
|
|
54
|
+
one-command bootstrap. From inside the target project:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
export PLANE_API_KEY=<key> # or LINEAR_API_KEY / TRELLO_KEY + TRELLO_TOKEN
|
|
@@ -65,9 +65,9 @@ macOS). See [Development guide: local install](development.md#local-install-one-
|
|
|
65
65
|
|
|
66
66
|
<!-- prettier-ignore -->
|
|
67
67
|
> [!IMPORTANT]
|
|
68
|
-
> Full
|
|
69
|
-
>
|
|
70
|
-
>
|
|
68
|
+
> Full provisioning is outward-facing. It can configure a Telegram bot and a
|
|
69
|
+
> Plane project, and the Telegram step is interactive. Use `install-local.sh`
|
|
70
|
+
> or the `SKIP_*` flags described in
|
|
71
71
|
> [Development guide:
|
|
72
72
|
> provisioning](development.md#provisioning-the-pm-manual) for local or lean
|
|
73
73
|
> installs.
|
|
@@ -83,9 +83,9 @@ All paths are relative to the repository root.
|
|
|
83
83
|
| `template/.scripts/lib/ticket-provider.sh` | The adapter dispatcher (`tp`). The engine's only seam to a ticket system. |
|
|
84
84
|
| `template/.scripts/providers/{linear,plane,trello}.sh` | The provider adapters. |
|
|
85
85
|
| `template/.scripts/42-ticket-provider.sh` | Provisioning step that resolves or creates the board. |
|
|
86
|
-
| `template/.scripts/70-systemd.sh` | Provisioning step that installs the gateway
|
|
86
|
+
| `template/.scripts/70-systemd.sh` | Provisioning step that installs the profile gateway and board-reconciliation `heartbeat` timer. |
|
|
87
87
|
| `install-local.sh` | One-command local install (no cloud, macOS + Linux). |
|
|
88
|
-
| `template/.scripts/heartbeat.sh` | The heartbeat runner:
|
|
88
|
+
| `template/.scripts/heartbeat.sh` | The heartbeat runner: sentinel full-pass dispatch with its own cooldown and lock. |
|
|
89
89
|
| `template/.scripts/sentinel.prompt.md.jinja` | The prompt the runner feeds to Hermes for a full reconciliation pass (rendered to `.scripts/sentinel.prompt.md`). |
|
|
90
90
|
| `template/.scripts/sentinel/bin/` | Enforcement tools: `issue-autonomous-review.sh`, `issue-close-gate.sh`, `emit-event.py`. |
|
|
91
91
|
| `template/.scripts/sentinel/docs/` | Runtime protocol docs shipped to each deployment. |
|
|
@@ -22,12 +22,11 @@ provisioning](development.md#provisioning-the-pm-manual).
|
|
|
22
22
|
## The heartbeat loop
|
|
23
23
|
|
|
24
24
|
A scheduler (`systemd` timer on Linux, `launchd` agent on macOS) fires the
|
|
25
|
-
runner, `template/.scripts/heartbeat.sh`, about once a minute.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
keeps cost low while staying responsive.
|
|
25
|
+
runner, `template/.scripts/heartbeat.sh`, about once a minute. For a pure-local
|
|
26
|
+
runtime, each tick performs a board-reconciliation **sentinel pass**. The cheap
|
|
27
|
+
heartbeat decides whether a full, LLM-backed reconciliation pass is worth
|
|
28
|
+
running. This keeps cost low while staying responsive; runtime backup remains
|
|
29
|
+
an operator-managed filesystem concern outside the sentinel.
|
|
31
30
|
|
|
32
31
|
The sentinel reads the work-state file,
|
|
33
32
|
`runtime/continuous-ticket-sentinel-state.json`, and chooses one of these
|
|
@@ -193,8 +192,7 @@ A full pass moves through these components in order:
|
|
|
193
192
|
which render an `accepted` or `held` verdict and emit a decision event; the
|
|
194
193
|
loop acts on the verdict (treat as done and unblock dependents, or send the
|
|
195
194
|
ticket back to active) without waiting on the operator.
|
|
196
|
-
6. The runner writes the outcome to the state file for the next tick
|
|
197
|
-
opportunistically checkpoints the runtime (gated to ~hourly) before exiting.
|
|
195
|
+
6. The runner writes the outcome to the state file for the next tick and exits.
|
|
198
196
|
|
|
199
197
|
## Read next
|
|
200
198
|
|
|
@@ -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
|