@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).parents[1]
|
|
11
|
+
SCRIPT = ROOT / "template" / ".mise" / "scripts" / "sync-skills.py"
|
|
12
|
+
PROVISION_SCRIPT = ROOT / "template" / ".mise" / "scripts" / "provision-bmad-skills.py"
|
|
13
|
+
SPEC = importlib.util.spec_from_file_location("sync_skills", SCRIPT)
|
|
14
|
+
assert SPEC is not None and SPEC.loader is not None
|
|
15
|
+
SYNC = importlib.util.module_from_spec(SPEC)
|
|
16
|
+
SPEC.loader.exec_module(SYNC)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SyncSkillsTopologyTests(unittest.TestCase):
|
|
20
|
+
def setUp(self) -> None:
|
|
21
|
+
self.temporary = tempfile.TemporaryDirectory(prefix="commonproject-sync-topology-")
|
|
22
|
+
self.root = Path(self.temporary.name)
|
|
23
|
+
self.project = self.root / "project"
|
|
24
|
+
self.source = self.root / "source-skill"
|
|
25
|
+
self.project.mkdir()
|
|
26
|
+
self.source.mkdir()
|
|
27
|
+
(self.source / "SKILL.md").write_text("source\n")
|
|
28
|
+
|
|
29
|
+
def tearDown(self) -> None:
|
|
30
|
+
self.temporary.cleanup()
|
|
31
|
+
|
|
32
|
+
def test_shipped_skill_scripts_are_executable(self) -> None:
|
|
33
|
+
for script in (PROVISION_SCRIPT, SCRIPT):
|
|
34
|
+
self.assertNotEqual(
|
|
35
|
+
script.stat().st_mode & 0o111,
|
|
36
|
+
0,
|
|
37
|
+
f"fresh template script must be executable: {script}",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def test_canonical_claude_alias_is_accepted_without_mutating_managed_projection(self) -> None:
|
|
41
|
+
managed = self.project / ".agents" / "skills"
|
|
42
|
+
custom = managed / "custom-real-skill"
|
|
43
|
+
custom.mkdir(parents=True)
|
|
44
|
+
(custom / "SKILL.md").write_text("custom\n")
|
|
45
|
+
claude = self.project / ".claude"
|
|
46
|
+
claude.mkdir()
|
|
47
|
+
(claude / "skills").symlink_to("../.agents/skills", target_is_directory=True)
|
|
48
|
+
|
|
49
|
+
SYNC.fanout_to_cli(self.project, {"managed-example": self.source})
|
|
50
|
+
|
|
51
|
+
self.assertTrue((claude / "skills").is_symlink())
|
|
52
|
+
self.assertEqual(os.readlink(claude / "skills"), "../.agents/skills")
|
|
53
|
+
self.assertEqual((custom / "SKILL.md").read_text(), "custom\n")
|
|
54
|
+
self.assertFalse(
|
|
55
|
+
(managed / "managed-example").exists(),
|
|
56
|
+
"the canonical alias must not make sync-skills mutate the provisioner-owned projection",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def test_external_cli_symlink_fails_before_any_destination_mutation(self) -> None:
|
|
60
|
+
codex = self.project / ".codex"
|
|
61
|
+
codex.mkdir()
|
|
62
|
+
claude = self.project / ".claude"
|
|
63
|
+
claude.mkdir()
|
|
64
|
+
outside = self.root / "outside"
|
|
65
|
+
outside.mkdir()
|
|
66
|
+
(outside / "sentinel").write_text("do-not-touch\n")
|
|
67
|
+
(claude / "skills").symlink_to(outside, target_is_directory=True)
|
|
68
|
+
|
|
69
|
+
with self.assertRaisesRegex(ValueError, "Refusing symlinked CLI skills directory"):
|
|
70
|
+
SYNC.fanout_to_cli(self.project, {"managed-example": self.source})
|
|
71
|
+
|
|
72
|
+
self.assertFalse((codex / "skills").exists())
|
|
73
|
+
self.assertEqual(list(outside.iterdir()), [outside / "sentinel"])
|
|
74
|
+
self.assertEqual((outside / "sentinel").read_text(), "do-not-touch\n")
|
|
75
|
+
|
|
76
|
+
def test_broken_canonical_alias_fails_before_mutation(self) -> None:
|
|
77
|
+
codex = self.project / ".codex"
|
|
78
|
+
codex.mkdir()
|
|
79
|
+
claude = self.project / ".claude"
|
|
80
|
+
claude.mkdir()
|
|
81
|
+
(claude / "skills").symlink_to("../.agents/skills", target_is_directory=True)
|
|
82
|
+
|
|
83
|
+
with self.assertRaisesRegex(ValueError, "alias target is not a real directory"):
|
|
84
|
+
SYNC.fanout_to_cli(self.project, {"managed-example": self.source})
|
|
85
|
+
|
|
86
|
+
self.assertFalse((codex / "skills").exists())
|
|
87
|
+
self.assertTrue((claude / "skills").is_symlink())
|
|
88
|
+
|
|
89
|
+
def test_parent_swap_after_preflight_cannot_mutate_outside_project(self) -> None:
|
|
90
|
+
codex = self.project / ".codex"
|
|
91
|
+
codex.mkdir()
|
|
92
|
+
outside = self.root / "outside"
|
|
93
|
+
outside_skill = outside / "skills" / "managed-example"
|
|
94
|
+
outside_skill.mkdir(parents=True)
|
|
95
|
+
(outside / "sentinel").write_text("outside must survive\n")
|
|
96
|
+
(outside_skill / "user-data").write_text("do not delete\n")
|
|
97
|
+
active = SYNC.preflight_cli_dirs(self.project, ["managed-example"])
|
|
98
|
+
original_codex = self.project / ".codex-original"
|
|
99
|
+
|
|
100
|
+
def swap_parent() -> None:
|
|
101
|
+
codex.rename(original_codex)
|
|
102
|
+
codex.symlink_to(outside, target_is_directory=True)
|
|
103
|
+
|
|
104
|
+
with self.assertRaisesRegex(ValueError, "symlinked destination directory"):
|
|
105
|
+
SYNC.fanout_to_cli(
|
|
106
|
+
self.project,
|
|
107
|
+
{"managed-example": self.source},
|
|
108
|
+
active_cli_dirs=active,
|
|
109
|
+
before_mutation=swap_parent,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.assertTrue(codex.is_symlink())
|
|
113
|
+
self.assertFalse((original_codex / "skills").exists())
|
|
114
|
+
self.assertEqual((outside / "sentinel").read_text(), "outside must survive\n")
|
|
115
|
+
self.assertEqual((outside_skill / "user-data").read_text(), "do not delete\n")
|
|
116
|
+
self.assertTrue(outside_skill.is_dir())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
unittest.main()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# hermes-agent-template
|
|
2
2
|
|
|
3
3
|
Copier template that provisions a single Hermes agent role into an existing
|
|
4
|
-
repository, complete with its own per-agent runtime
|
|
5
|
-
memory
|
|
4
|
+
repository, complete with its own ignored per-agent runtime directory for
|
|
5
|
+
memory and session state.
|
|
6
6
|
|
|
7
7
|
## How it relates to CommonProject
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ memory/state checkpointing.
|
|
|
11
11
|
| What it scaffolds | A new top-level project | An agent role inside an existing project |
|
|
12
12
|
| Copier target | `./my-new-project/` | `./agents/hermes/<role>/` |
|
|
13
13
|
| Asks | project name + description | role + purpose + tone |
|
|
14
|
-
| Post-gen artifacts | Plane project, bmad init, mise tasks | Plane project, Telegram bot wiring,
|
|
14
|
+
| Post-gen artifacts | Plane project, bmad init, mise tasks | Plane project, Telegram bot wiring, optional Slack wiring, pure-local runtime, systemd units |
|
|
15
15
|
|
|
16
16
|
CommonProject runs first to create the umbrella project. hermes-agent-template
|
|
17
17
|
runs second (for each agent role you want) to drop agents into it.
|
|
@@ -22,9 +22,10 @@ The template provisions a single Hermes role per invocation. The `pm` role
|
|
|
22
22
|
handles project management and triage, and also runs the continuous ticket
|
|
23
23
|
sentinel out-of-band: a provider-agnostic board-reconciliation pass (Linear,
|
|
24
24
|
Plane, or Trello) with an autonomous adversarial review (act, do not wait). The
|
|
25
|
-
sentinel runs as the PM's **heartbeat**
|
|
26
|
-
|
|
27
|
-
separate `scrum-master` role; its duties folded into the PM
|
|
25
|
+
sentinel runs as the PM's **heartbeat** systemd timer. Under the current
|
|
26
|
+
pure-local runtime contract, that tick performs board reconciliation only.
|
|
27
|
+
(There is no separate `scrum-master` role; its duties folded into the PM
|
|
28
|
+
heartbeat.)
|
|
28
29
|
|
|
29
30
|
To work on or extend the heartbeat sentinel, start with the [sentinel handoff
|
|
30
31
|
guide](docs/sentinel/README.md).
|
|
@@ -43,13 +44,15 @@ The template will:
|
|
|
43
44
|
1. Seed `~/.config/hermes-agent-template/config.toml` from the shipped example (see [Configuration](#configuration))
|
|
44
45
|
2. Ensure `~/.hermes/fleet.env` exists (single source of truth for shared Hermes binary/repo/registry)
|
|
45
46
|
3. Create the hermes profile `<repo>-pm` via `hermes profile create --clone`
|
|
46
|
-
4. Create
|
|
47
|
-
5. Populate
|
|
48
|
-
6.
|
|
49
|
-
7.
|
|
50
|
-
8.
|
|
51
|
-
9.
|
|
52
|
-
10.
|
|
47
|
+
4. Create ignored local state at `agents/hermes/pm/runtime/` (== HERMES_HOME)
|
|
48
|
+
5. Populate missing files from the runtime scaffold (config.yaml, SOUL.md, memories)
|
|
49
|
+
6. Refuse any stale project gitlink or `.gitmodules` mapping for that runtime
|
|
50
|
+
7. Verify a profile-dedicated BotFather token and store it only in `runtime/.env`
|
|
51
|
+
8. Defer Slack by default, or verify and store an explicitly supplied dedicated Slack app+bot pair in `runtime/.env`
|
|
52
|
+
9. Create a Plane project in your configured workspace
|
|
53
|
+
10. Mark Bloodbank ingress as fleet-scoped, with no per-profile consumer
|
|
54
|
+
11. Install systemd `--user` units: profile gateway and board-reconciliation heartbeat timer
|
|
55
|
+
12. Append the agent and its Bloodbank `target_agent_id` to `~/.hermes/agents-registry.yaml`
|
|
53
56
|
|
|
54
57
|
## Configuration
|
|
55
58
|
|
|
@@ -65,13 +68,16 @@ you to review it. Keys:
|
|
|
65
68
|
| Section | Key | What it sets |
|
|
66
69
|
| --- | --- | --- |
|
|
67
70
|
| `fleet` | `hermes_bin`, `hermes_repo` | Shared Hermes executable + repo checkout |
|
|
71
|
+
| `fleet` | `hermes_git_url`, `hermes_git_ref`, `hermes_git_sha` | Reviewed Hermes fork publication used by clean installs and fleet audit metadata |
|
|
68
72
|
| `fleet` | `fleet_env`, `registry_file` | Fleet source-of-truth + registry locations |
|
|
69
73
|
| `fleet` | `oauth_file`, `codex_home` | Shared Hermes OAuth store + Codex CLI/app-server auth home |
|
|
70
74
|
| `fleet` | `runtime_scaffold_dir` | Fallback scaffold (if agent-local one is missing) |
|
|
71
75
|
| `fleet` | `canonical_skills_dir`, `symlinked_runtime_skills` | Skills mirrored into each profile |
|
|
72
|
-
| `github` | `runtime_repo_owner` | Owner of the `agent-hm-*` runtime repos |
|
|
73
76
|
| `plane` | `base`, `workspace` | Plane URL + workspace slug |
|
|
74
|
-
|
|
77
|
+
|
|
78
|
+
The example configuration retains an inert `[github].runtime_repo_owner` value
|
|
79
|
+
only so older manifests can still be parsed. Current provisioning never reads
|
|
80
|
+
it to create, attach, synchronize, restore, or retire runtime storage.
|
|
75
81
|
|
|
76
82
|
Resolution precedence for every value: **explicit env var → `~/.hermes/fleet.env`
|
|
77
83
|
→ `config.toml` → built-in fallback**. So you can still override any single value
|
|
@@ -87,24 +93,51 @@ your-project/
|
|
|
87
93
|
│ ├── SOUL.md ← personality (canonical)
|
|
88
94
|
│ ├── hermes ← launcher
|
|
89
95
|
│ ├── .scripts/ ← provisioning scripts (idempotent re-run)
|
|
90
|
-
│ └── runtime/ ←
|
|
96
|
+
│ └── runtime/ ← ignored local HERMES_HOME
|
|
91
97
|
│ (HERMES_HOME for this agent)
|
|
98
|
+
│ └── .env ← profile-local channel credentials, mode 0600, gitignored
|
|
92
99
|
└── ...
|
|
93
100
|
|
|
94
|
-
github.com/delorenj/agent-hm-<repo>-pm/ ← new private repo, this agent's state
|
|
95
|
-
├── config.yaml ← cloned from global ~/.hermes
|
|
96
|
-
├── SOUL.md ← evolves over time
|
|
97
|
-
├── memories/{MEMORY,USER}.md
|
|
98
|
-
├── sessions/sessions.db ← LFS-tracked
|
|
99
|
-
├── decisions/ ← markdown files, one per call
|
|
100
|
-
├── bloodbank-consumer.py
|
|
101
|
-
└── .gitattributes / .gitignore ← LFS rules + secret guards
|
|
102
|
-
|
|
103
101
|
~/.hermes/agents-registry.yaml ← fleet roster
|
|
104
102
|
~/.hermes/fleet.env ← shared Hermes binary/repo pointer
|
|
105
|
-
~/.config/systemd/user/ ← gateway
|
|
103
|
+
~/.config/systemd/user/ ← per-profile gateway + heartbeat timer
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Bloodbank command ingress is not a per-profile daemon. One fleet-shared Hermes
|
|
107
|
+
gateway reads the registry and routes canonical commands by
|
|
108
|
+
`data.target_agent_id`; local runtime directories contain no NATS consumer or
|
|
109
|
+
inbox bridge.
|
|
110
|
+
Provisioning and `fleet-sync.sh --apply` also retire the old
|
|
111
|
+
`hermes-<agent>-consumer.service`, even when an older `.done-70-systemd` marker
|
|
112
|
+
exists. Retirement fails closed: a user-manager/query error, failed disable, or
|
|
113
|
+
anything short of explicit `inactive` plus `disabled` leaves the unit file and
|
|
114
|
+
registry metadata intact and reports unhealthy drift.
|
|
115
|
+
|
|
116
|
+
Telegram and Slack ownership checks, identity claims, runtime credential
|
|
117
|
+
writes, and registry upserts serialize on `${registry_file}.lock`. The lock is
|
|
118
|
+
held by `flock`, so a crashed process cannot leave a stale logical lock; registry
|
|
119
|
+
writes use an atomic replace, sync the containing directory where supported, and
|
|
120
|
+
keep both registry and lock at mode `0600`. Profile credential replacements use
|
|
121
|
+
the same file-plus-parent durability boundary and remain safe to retry when a
|
|
122
|
+
durability sync reports an error.
|
|
123
|
+
|
|
124
|
+
## Reviewed Hermes runtime publication
|
|
125
|
+
|
|
126
|
+
Clean installs use only the reviewed fleet fork publication below. The local
|
|
127
|
+
installer verifies the commit belongs to the named ref and refuses an existing
|
|
128
|
+
checkout whose `origin` points elsewhere:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
git clone --branch feature/PJAN-19-routing-publication --single-branch \
|
|
132
|
+
https://github.com/delorenj/hermes-agent.git ~/.hermes/hermes-agent
|
|
133
|
+
git -C ~/.hermes/hermes-agent rev-parse HEAD
|
|
134
|
+
# 113e1b182b6d72a7dd02a191f134a41668ceaf0e
|
|
106
135
|
```
|
|
107
136
|
|
|
137
|
+
Do not install this fleet path from `NousResearch/hermes-agent` or mutate its
|
|
138
|
+
`main` branch. Promotion happens on the fork publication ref and is pinned by
|
|
139
|
+
full commit SHA in `config.example.toml` and each registry entry.
|
|
140
|
+
|
|
108
141
|
## Fleet single source-of-truth
|
|
109
142
|
|
|
110
143
|
All generated launchers read `~/.hermes/fleet.env` for:
|
|
@@ -120,6 +153,12 @@ agent wrapper picks it up automatically. `HERMES_FLEET_OAUTH_FILE` is the
|
|
|
120
153
|
shared Hermes provider OAuth store, including `openai-codex`; `HERMES_FLEET_CODEX_HOME`
|
|
121
154
|
is the shared Codex CLI/app-server config/auth home.
|
|
122
155
|
|
|
156
|
+
Telegram bot tokens and Slack bot/app tokens are deliberately excluded from
|
|
157
|
+
this shared layer. They belong only in the enabled agent's `runtime/.env` and
|
|
158
|
+
are checked for local profile ownership during provisioning; fleet config may
|
|
159
|
+
carry the non-secret `TELEGRAM_ALLOWED_USERS` and `SLACK_ALLOWED_USERS`
|
|
160
|
+
policies as a convenience.
|
|
161
|
+
|
|
123
162
|
To retrofit existing wrappers and user systemd units:
|
|
124
163
|
|
|
125
164
|
```bash
|
|
@@ -146,6 +185,8 @@ SKIP_TELEGRAM=1 SKIP_SYSTEMD=1 ./.scripts/40-plane.sh
|
|
|
146
185
|
|
|
147
186
|
(TODO: ship `retire.sh` in v1.1)
|
|
148
187
|
|
|
149
|
-
Manual: stop systemd units,
|
|
150
|
-
|
|
151
|
-
|
|
188
|
+
Manual retirement is deliberately non-destructive: stop the systemd units,
|
|
189
|
+
detach the profile symlink, archive the Plane project, retire messaging bots,
|
|
190
|
+
and remove the registry entry while preserving the ignored runtime directory.
|
|
191
|
+
The verified-backup and no-automated-purge retention policy is in
|
|
192
|
+
[Operations](docs/operations.md#retire-an-agent-preserves-runtime-by-default).
|
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
#
|
|
11
11
|
# What it does:
|
|
12
12
|
# 1. Renders agents/hermes/<role>/{role.yaml, SOUL.md, hermes, .scripts/}
|
|
13
|
-
# 2. Creates
|
|
14
|
-
#
|
|
13
|
+
# 2. Creates ignored local state at agents/hermes/<role>/runtime/
|
|
14
|
+
# (this is HERMES_HOME; no project gitlink or runtime remote)
|
|
15
15
|
# 4. Creates a Plane project in 33god workspace
|
|
16
16
|
# 6. Prompts for a BotFather Telegram token, stores in runtime/.env
|
|
17
|
-
# 7.
|
|
18
|
-
# 8.
|
|
17
|
+
# 7. Optionally wires a dedicated Slack app+bot pair into runtime/.env
|
|
18
|
+
# 8. Records fleet-shared Bloodbank routing identity (no profile consumer)
|
|
19
|
+
# 9. Installs a profile gateway + heartbeat timer (reconcile + checkpoint)
|
|
20
|
+
# 10. Appends to ~/.hermes/agents-registry.yaml
|
|
19
21
|
#
|
|
20
|
-
# Environment-specific defaults (Hermes binary path,
|
|
22
|
+
# Environment-specific defaults (Hermes binary path, legacy archive owner, Plane
|
|
21
23
|
# workspace/URL, NATS endpoint, skills dir, …) are NOT hardcoded — they live in
|
|
22
24
|
# ~/.config/hermes-agent-template/config.toml, seeded from config.example.toml on
|
|
23
25
|
# first run. Edit that file to retarget the template for a different machine/user.
|
|
@@ -113,7 +115,7 @@ runtime_repo:
|
|
|
113
115
|
|
|
114
116
|
runtime_repo_owner:
|
|
115
117
|
type: str
|
|
116
|
-
|
|
118
|
+
# Legacy archive identity only. Empty by default — resolved at provision time from
|
|
117
119
|
# ~/.config/hermes-agent-template/config.toml [github].runtime_repo_owner.
|
|
118
120
|
# Pass --data runtime_repo_owner=<owner> to override per-run.
|
|
119
121
|
default: ""
|
|
@@ -138,6 +140,7 @@ _tasks:
|
|
|
138
140
|
- "./.scripts/10-hermes-profile.sh"
|
|
139
141
|
- "./.scripts/20-runtime-repo.sh"
|
|
140
142
|
- "./.scripts/30-telegram.sh"
|
|
143
|
+
- "./.scripts/31-slack.sh"
|
|
141
144
|
- "./.scripts/42-ticket-provider.sh"
|
|
142
145
|
- "./.scripts/60-bloodbank.sh"
|
|
143
146
|
- "./.scripts/70-systemd.sh"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Tracked role and local runtime split
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
┌──────────────────────────────────────────────────────────────────────────┐
|
|
@@ -20,77 +20,79 @@
|
|
|
20
20
|
│ ├── SOUL.md │
|
|
21
21
|
│ ├── hermes (launcher) │
|
|
22
22
|
│ ├── .scripts/ │
|
|
23
|
-
│ └── runtime/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
│
|
|
28
|
-
|
|
29
|
-
│ ├── config.yaml │
|
|
30
|
-
│ ├── SOUL.md (evolving) │
|
|
31
|
-
│ ├── memories/ │ auto-checkpointed
|
|
32
|
-
│ ├── sessions/sessions.db (LFS) │ by the heartbeat
|
|
33
|
-
│ ├── decisions/ │ + on session end
|
|
34
|
-
│ └── bloodbank-consumer.py │
|
|
35
|
-
└────────────────────────────────────────┘
|
|
23
|
+
│ └── runtime/ │ ← ignored local HERMES_HOME
|
|
24
|
+
│ ├── config.yaml │
|
|
25
|
+
│ ├── memories/ │
|
|
26
|
+
│ ├── sessions/ │
|
|
27
|
+
│ └── .env │
|
|
28
|
+
└─────────────────────────────────────────┘
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
## Why
|
|
31
|
+
## Why tracked role and local state are separate
|
|
39
32
|
|
|
40
33
|
The **template** is the contract / the bootstrap recipe — it doesn't change
|
|
41
|
-
when an agent learns something. The **runtime** is the
|
|
42
|
-
state — it changes every conversation.
|
|
43
|
-
|
|
34
|
+
when an agent learns something. The ignored **runtime directory** is the
|
|
35
|
+
agent's accumulating local state — it changes every conversation. Separating
|
|
36
|
+
them means:
|
|
44
37
|
|
|
45
38
|
- The template repo is small, stable, easy to update fleet-wide
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
39
|
+
- Project commits cannot accidentally publish runtime credentials or sessions
|
|
40
|
+
- Each profile has an isolated HERMES_HOME without project-index churn
|
|
41
|
+
- Provisioning can refresh tracked launchers and scaffolds without overwriting
|
|
42
|
+
existing local state
|
|
49
43
|
|
|
50
44
|
A third file ties the fleet together: `~/.hermes/fleet.env`.
|
|
51
45
|
It is the single source-of-truth pointer for the shared Hermes executable/repo
|
|
52
46
|
that every generated launcher uses.
|
|
53
47
|
|
|
54
|
-
##
|
|
48
|
+
## Durability boundary
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
Ignored local state is not automatically durable. The operator must configure
|
|
51
|
+
an encrypted filesystem backup or snapshot for each exact runtime path. A
|
|
52
|
+
project clone restores only tracked role files and the empty scaffold.
|
|
53
|
+
Hindsight retains only memories/events explicitly written to its bank, while a
|
|
54
|
+
secret manager retains only credentials explicitly stored there; neither is a
|
|
55
|
+
complete runtime backup. See [Operations](operations.md#back-up-and-restore-an-agent).
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
- **Auditability**: `git log` is a full trace of how the agent evolved.
|
|
62
|
-
- **Reversibility**: if the agent develops bad habits, `git revert` rolls back.
|
|
63
|
-
- **Forkability**: experiment with a copy on a branch, merge if it works out.
|
|
64
|
-
- **Cross-machine**: same agent state on big-chungus and on the laptop.
|
|
65
|
-
|
|
66
|
-
## Heartbeat cadence (reconcile + checkpoint)
|
|
57
|
+
## Heartbeat cadence
|
|
67
58
|
|
|
68
59
|
A systemd `--user` timer runs `.scripts/heartbeat.sh` frequently (about once a
|
|
69
|
-
minute).
|
|
60
|
+
minute). For a pure-local runtime, each tick performs one job:
|
|
70
61
|
|
|
71
62
|
1. **Board-reconciliation sentinel pass** — the PM's continuous ticket sentinel.
|
|
72
63
|
The runner's own cooldown/lock logic decides whether a full, LLM-backed
|
|
73
64
|
reconciliation pass is worth running (it rate-limits the expensive Hermes
|
|
74
65
|
call); see [the sentinel docs](sentinel/README.md).
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
On session end, a hermes hook (TBD path) checkpoints immediately so nothing
|
|
81
|
-
in-flight is lost between heartbeat ticks.
|
|
82
|
-
|
|
83
|
-
Sensitive state — `.env`, `auth.json`, OAuth tokens — never enters git.
|
|
84
|
-
They're in `.gitignore` and live only on the host machine.
|
|
66
|
+
Sensitive state — `.env`, `auth.json`, OAuth tokens — never enters project Git.
|
|
67
|
+
It lives only in ignored local storage unless the operator separately places a
|
|
68
|
+
credential in the secret manager or includes the runtime in an encrypted
|
|
69
|
+
filesystem backup.
|
|
85
70
|
|
|
86
71
|
## One bot per agent (Telegram)
|
|
87
72
|
|
|
88
73
|
Each agent gets its own BotFather bot and runs its own gateway daemon.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
74
|
+
The BotFather token is an invocation-only provisioning input: shared
|
|
75
|
+
`fleet.env` may carry the non-secret allow-list policy but is never allowed to
|
|
76
|
+
supply `TELEGRAM_BOT_TOKEN`. Provisioning verifies `getMe`, rejects a token or
|
|
77
|
+
bot identity already owned anywhere in the local fleet, atomically writes the
|
|
78
|
+
credential only to the profile's mode-`0600` `runtime/.env`, and records only
|
|
79
|
+
safe identity metadata in `role.yaml` and the registry. Hermes' scoped runtime
|
|
80
|
+
lock remains a second line of defense against duplicate pollers.
|
|
81
|
+
|
|
82
|
+
## One app and bot per Slack-enabled agent
|
|
83
|
+
|
|
84
|
+
Slack is opt-in and remains deferred for newly provisioned agents unless the
|
|
85
|
+
operator explicitly enables it or supplies both required tokens. An enabled
|
|
86
|
+
agent owns one dedicated `xapp-` Socket Mode token and one dedicated `xoxb-`
|
|
87
|
+
bot token; provisioning rejects token reuse and a verified bot identity already
|
|
88
|
+
owned by another registry entry.
|
|
89
|
+
|
|
90
|
+
The bot token is verified through Slack's read-only `auth.test` endpoint.
|
|
91
|
+
Credentials live only in the agent's mode-`0600`, gitignored `runtime/.env`.
|
|
92
|
+
The shared `~/.hermes/.env`, `fleet.env`, `role.yaml`, and fleet registry never
|
|
93
|
+
contain Slack tokens: manifests and registry entries retain only provisioning
|
|
94
|
+
status, workspace identity, and bot identity. The non-secret allowed-user
|
|
95
|
+
policy may be inherited from fleet config.
|
|
94
96
|
|
|
95
97
|
## One Plane project per agent
|
|
96
98
|
|
|
@@ -100,12 +102,34 @@ archive-on-retire clean.
|
|
|
100
102
|
|
|
101
103
|
## Bloodbank wiring
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
Bloodbank command ingress is owned by one fleet-shared official Hermes gateway,
|
|
106
|
+
not by a consumer in every runtime. Each registry entry advertises:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
bloodbank:
|
|
110
|
+
gateway_scope: fleet
|
|
111
|
+
target_agent_id: <agent-id>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The shared gateway subscribes once, resolves `data.target_agent_id` through the
|
|
115
|
+
fleet registry, and routes the turn into that Hermes profile. Per-profile
|
|
116
|
+
messaging gateways and heartbeat timers remain independent; there is no
|
|
117
|
+
per-profile NATS process, systemd consumer unit, or filesystem inbox bridge.
|
|
106
118
|
|
|
107
119
|
Each agent emits CloudEvents 1.0 envelopes with `actor.agent_id`,
|
|
108
120
|
`producer = hermes-agent:<id>`, `source = hermes://agent/<id>`. The naming
|
|
109
121
|
contract is owned by Bloodbank (`~/code/33GOD/bloodbank/docs/event-naming.md`).
|
|
110
122
|
Repo and agent identifiers belong in envelope data, actor, or source fields,
|
|
111
123
|
never in type or subject tokens.
|
|
124
|
+
|
|
125
|
+
The gateway uses the canonical lifecycle already defined by those schemas:
|
|
126
|
+
|
|
127
|
+
- `bloodbank.v1.conversation.turn.started`
|
|
128
|
+
- `bloodbank.v1.agent.invocation.started`
|
|
129
|
+
- one terminal invocation event: `bloodbank.v1.agent.invocation.completed` or
|
|
130
|
+
`bloodbank.v1.agent.invocation.failed`
|
|
131
|
+
- `bloodbank.v1.conversation.turn.completed`
|
|
132
|
+
|
|
133
|
+
There are no separate `received` or `accepted` lifecycle events. A JetStream
|
|
134
|
+
command is acknowledged only after Hermes processing completion and terminal
|
|
135
|
+
event publication.
|
|
@@ -43,7 +43,10 @@ n8n
|
|
|
43
43
|
runs visual Fleet workflows generated from pjangler and registry state
|
|
44
44
|
|
|
45
45
|
systemd --user
|
|
46
|
-
keeps gateway
|
|
46
|
+
keeps each profile gateway and local fallback heartbeat/checkpoint alive
|
|
47
|
+
|
|
48
|
+
fleet Bloodbank gateway
|
|
49
|
+
routes canonical commands to registered target_agent_id values
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
## Architecture Decisions
|
|
@@ -115,7 +118,8 @@ Rationale:
|
|
|
115
118
|
|
|
116
119
|
Consequences:
|
|
117
120
|
|
|
118
|
-
- systemd keeps gateway and
|
|
121
|
+
- systemd keeps each profile gateway and heartbeat fallback running.
|
|
122
|
+
- The fleet-shared Bloodbank gateway owns command-bus ingress for all profiles.
|
|
119
123
|
- systemd fallback performs minimal self-health and checkpoint behavior.
|
|
120
124
|
- n8n performs supervisor flows, agent health fanout, delegation, and
|
|
121
125
|
reconciliation triggers.
|
|
@@ -164,8 +168,10 @@ systemd:
|
|
|
164
168
|
fallback: true
|
|
165
169
|
required_units:
|
|
166
170
|
- gateway
|
|
167
|
-
- consumer
|
|
168
171
|
- fallback-heartbeat
|
|
172
|
+
bloodbank:
|
|
173
|
+
gateway_scope: fleet
|
|
174
|
+
routing_key: data.target_agent_id
|
|
169
175
|
```
|
|
170
176
|
|
|
171
177
|
## n8n Workflow Model
|
|
@@ -205,8 +211,8 @@ Trigger:
|
|
|
205
211
|
Flow:
|
|
206
212
|
|
|
207
213
|
1. Load one agent entry.
|
|
208
|
-
2. Check gateway,
|
|
209
|
-
and role manifest.
|
|
214
|
+
2. Check profile gateway, fleet Bloodbank registration, fallback heartbeat,
|
|
215
|
+
profile symlink, runtime repo, and role manifest.
|
|
210
216
|
3. If safe drift exists, call `pj fleet reconcile --agent <id> --apply`.
|
|
211
217
|
4. If unsafe drift exists, emit manual action.
|
|
212
218
|
5. Record result for supervisor.
|
|
@@ -250,4 +256,3 @@ pj fleet n8n create --name <workflow>
|
|
|
250
256
|
4. Promote heartbeat v2 contract and systemd fallback behavior.
|
|
251
257
|
5. Generate and validate n8n supervisor workflow.
|
|
252
258
|
6. Enable workflow creation only after exported workflow validation passes.
|
|
253
|
-
|
|
@@ -201,7 +201,8 @@ models do not coexist accidentally.
|
|
|
201
201
|
Acceptance Criteria:
|
|
202
202
|
|
|
203
203
|
- Given `service_model: hybrid-n8n-systemd`, when validation runs, then required
|
|
204
|
-
gateway,
|
|
204
|
+
profile gateway, fleet Bloodbank registration, and fallback heartbeat
|
|
205
|
+
expectations are checked.
|
|
205
206
|
- Given a checkpoint-only agent exists during migration, when status runs, then
|
|
206
207
|
it is classified as legacy rather than broken.
|
|
207
208
|
- Given a heartbeat-enabled agent exists, when status runs, then n8n registration
|
|
@@ -277,4 +278,3 @@ Acceptance Criteria:
|
|
|
277
278
|
expected changes without mutation.
|
|
278
279
|
- Given n8n workflow code is generated, when validation runs, then no workflow is
|
|
279
280
|
created until validation succeeds.
|
|
280
|
-
|
|
@@ -95,8 +95,8 @@ NFR1: Default commands must be read-only or dry-run.
|
|
|
95
95
|
NFR2: Reconciliation must never destroy or merge runtime state automatically
|
|
96
96
|
when profiles or runtime repos contain ambiguous data.
|
|
97
97
|
|
|
98
|
-
NFR3: n8n outage must not prevent baseline agent health, gateway
|
|
99
|
-
|
|
98
|
+
NFR3: n8n outage must not prevent baseline agent health, profile gateway
|
|
99
|
+
operation, fleet Bloodbank routing, or runtime checkpoint fallback.
|
|
100
100
|
|
|
101
101
|
NFR4: Validation output must be deterministic and grep-friendly.
|
|
102
102
|
|
|
@@ -138,4 +138,3 @@ Out of scope for the first implementation:
|
|
|
138
138
|
- `npm run prepublishOnly` fails if vendored templates are dirty.
|
|
139
139
|
- A generated n8n Fleet supervisor workflow can be validated before creation.
|
|
140
140
|
- Agents continue local fallback behavior when n8n is unavailable.
|
|
141
|
-
|