@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.
Files changed (63) hide show
  1. package/dist/index.js +1852 -368
  2. package/dist/mcp-server.js +1782 -296
  3. package/package.json +14 -3
  4. package/templates/commonproject/copier.yml +3 -0
  5. package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
  6. package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
  7. package/templates/commonproject/template/mise.toml.jinja +9 -2
  8. package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
  9. package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
  10. package/templates/hermes-agent/README.md +70 -29
  11. package/templates/hermes-agent/copier.yml +9 -6
  12. package/templates/hermes-agent/docs/architecture.md +75 -51
  13. package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
  14. package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
  15. package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
  16. package/templates/hermes-agent/docs/operations.md +179 -81
  17. package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
  18. package/templates/hermes-agent/docs/sentinel/README.md +10 -10
  19. package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
  20. package/templates/hermes-agent/docs/sentinel/development.md +12 -15
  21. package/templates/hermes-agent/docs/sentinel.md +1 -1
  22. package/templates/hermes-agent/install-local.sh +41 -4
  23. package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
  24. package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
  25. package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
  26. package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
  27. package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
  28. package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
  29. package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
  30. package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
  31. package/templates/hermes-agent/template/.gitignore.jinja +5 -2
  32. package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
  33. package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
  34. package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
  35. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
  36. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
  37. package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
  38. package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
  39. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
  40. package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
  41. package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
  42. package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
  43. package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
  44. package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
  45. package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
  46. package/templates/hermes-agent/template/.scripts/heartbeat.sh +1 -1
  47. package/templates/hermes-agent/template/SOUL.md.jinja +2 -2
  48. package/templates/hermes-agent/template/hermes.jinja +6 -3
  49. package/templates/hermes-agent/template/role.yaml.jinja +24 -13
  50. package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
  51. package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
  52. package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
  53. package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
  54. package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
  55. package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
  56. package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
  57. package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
  58. package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
  59. package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
  60. package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
  61. package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
  62. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
  63. 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 repo for git-tracked
5
- memory/state checkpointing.
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, agent-hm-* runtime repo, systemd units |
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** — a fused systemd timer tick that does
26
- the reconciliation pass and then a gated runtime checkpoint. (There is no
27
- separate `scrum-master` role; its duties folded into the PM heartbeat.)
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 a new private GitHub repo `<owner>/agent-hm-<repo>-pm` for the runtime
47
- 5. Populate it with the runtime scaffold (config.yaml, SOUL.md, memories, consumer.py)
48
- 6. Add it as a git submodule at `agents/hermes/pm/runtime/` (== HERMES_HOME)
49
- 7. Prompt for a BotFather token, store it in `runtime/.env`
50
- 8. Create a Plane project in your configured workspace
51
- 9. Install systemd `--user` units: gateway, consumer, heartbeat timer (reconcile + checkpoint)
52
- 10. Append the agent to `~/.hermes/agents-registry.yaml`
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
- | `bloodbank` | `nats_host`, `nats_port`, `compose_dir` | NATS endpoint + compose dir hint |
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/ ← git submodule → agent-hm-<repo>-pm
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, consumer, heartbeat timer
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, `hermes profile delete`, archive the Plane
150
- project, `/deletebot` in BotFather, archive the
151
- `agent-hm-*` runtime repo, remove the registry entry.
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 a per-agent runtime repo gh:delorenj/agent-hm-<repo>-<role>
14
- # 3. Submodules it at agents/hermes/<role>/runtime/ (this is HERMES_HOME)
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. Installs a Bloodbank consumer + heartbeat timer (reconcile + checkpoint)
18
- # 8. Appends to ~/.hermes/agents-registry.yaml
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, runtime repo owner, Plane
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
- # Empty by default — resolved at provision time from
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
- ## Two-artifact split
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/ ────────────┼─── git submodule
24
- └────────────────────────────────┘
25
-
26
- ┌────────────────────────────────────────┐
27
- gh:delorenj/agent-hm-<project>-<role> ← NEW repo per agent
28
- │ ───────────────────────────────────── │ private
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 two artifacts, not one
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 agent's accumulating
42
- state — it changes every conversation. Bundling them would mean every memory
43
- update churns the template's commit log; separating them means:
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
- - The runtime repo is per-agent, fast-moving, auditable in isolation
47
- - You can fork an agent (branch the runtime repo) without touching others
48
- - You can wipe an agent (delete the runtime repo) without affecting the template
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
- ## Why git-tracked runtime
48
+ ## Durability boundary
55
49
 
56
- The runtime is the agent's "subjective experience" its memory of every
57
- conversation, the SOUL refinements it has absorbed, the decisions it has
58
- emitted. Putting it in git gives:
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
- - **Durability**: nothing lost when the host dies. `git clone` restores it.
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). Each tick fuses two jobs into one:
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
- 2. **Gated runtime checkpoint** after the sentinel decision, the runner calls
76
- `.scripts/checkpoint.sh`, gated to at most once an hour. The checkpoint:
77
- `cd`s into the runtime submodule, `git add -A`, commits only if dirty (exits
78
- clean otherwise), and pushes to `origin`.
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
- Hermes' `gateway/status.py:acquire_scoped_lock(scope="telegram", identity=<token>)`
90
- already enforces "one token per gateway process" so even if two profiles
91
- happen to share a token, the second one's startup fails fast. The N×M cost
92
- (N BotFather sessions per fleet) is the price we accept for zero custom
93
- routing code.
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
- Each consumer subscribes to two lanes:
104
- - `bloodbank.evt.v1.repo.>` canonical repo-domain events, filtered by `data.repo`
105
- - `bloodbank.cmd.v1.agent.>` — canonical agent-domain commands, filtered by `data.target_agent_id`
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, consumer, and local fallback heartbeat/checkpoint alive
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 consumer services running.
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, consumer, fallback heartbeat, profile symlink, runtime repo,
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, consumer, and fallback heartbeat expectations are checked.
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 operation,
99
- consumer operation, or runtime checkpoint fallback.
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
-