@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.
Files changed (72) hide show
  1. package/dist/index.js +2353 -518
  2. package/dist/mcp-server.js +2331 -494
  3. package/package.json +14 -3
  4. package/templates/commonproject/copier.yml +6 -4
  5. package/templates/commonproject/template/.agents/hooks/README.md +13 -26
  6. package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
  7. package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
  8. package/templates/commonproject/template/.agents/local.example.json +2 -8
  9. package/templates/commonproject/template/.agents/skills.json +6 -0
  10. package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
  11. package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
  12. package/templates/commonproject/template/mise.toml.jinja +16 -16
  13. package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
  14. package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
  15. package/templates/hermes-agent/README.md +70 -29
  16. package/templates/hermes-agent/copier.yml +9 -6
  17. package/templates/hermes-agent/docs/architecture.md +75 -51
  18. package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
  19. package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
  20. package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
  21. package/templates/hermes-agent/docs/operations.md +179 -81
  22. package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
  23. package/templates/hermes-agent/docs/sentinel/README.md +10 -10
  24. package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
  25. package/templates/hermes-agent/docs/sentinel/development.md +12 -15
  26. package/templates/hermes-agent/docs/sentinel.md +1 -1
  27. package/templates/hermes-agent/install-local.sh +41 -4
  28. package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
  29. package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
  30. package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
  31. package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
  32. package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
  33. package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
  34. package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
  35. package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
  36. package/templates/hermes-agent/template/.gitignore.jinja +5 -2
  37. package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
  38. package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
  39. package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
  40. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
  41. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
  42. package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
  43. package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
  44. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
  45. package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
  46. package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
  47. package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
  48. package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
  49. package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
  50. package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
  51. package/templates/hermes-agent/template/.scripts/heartbeat.sh +12 -1
  52. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
  53. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +21 -0
  54. package/templates/hermes-agent/template/SOUL.md.jinja +36 -15
  55. package/templates/hermes-agent/template/hermes.jinja +6 -3
  56. package/templates/hermes-agent/template/role.yaml.jinja +24 -13
  57. package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
  58. package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
  59. package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
  60. package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
  61. package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
  62. package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
  63. package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
  64. package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
  65. package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
  66. package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
  67. package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
  68. package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
  69. package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
  70. package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
  71. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
  72. package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +0 -181
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import json
5
+ import os
6
+ import shutil
7
+ import stat
8
+ import tempfile
9
+ import unittest
10
+ from contextlib import contextmanager
11
+ from pathlib import Path
12
+
13
+
14
+ ROOT = Path(__file__).parents[1]
15
+ SCRIPT = ROOT / "template" / ".mise" / "scripts" / "provision-bmad-skills.py"
16
+ SPEC = importlib.util.spec_from_file_location("provision_bmad_skills", SCRIPT)
17
+ assert SPEC is not None and SPEC.loader is not None
18
+ PROVISIONER = importlib.util.module_from_spec(SPEC)
19
+ SPEC.loader.exec_module(PROVISIONER)
20
+
21
+
22
+ @contextmanager
23
+ def working_directory(path: Path):
24
+ previous = Path.cwd()
25
+ os.chdir(path)
26
+ try:
27
+ yield
28
+ finally:
29
+ os.chdir(previous)
30
+
31
+
32
+ def snapshot(path: Path):
33
+ try:
34
+ mode = path.lstat().st_mode
35
+ except FileNotFoundError:
36
+ return ("missing",)
37
+ permissions = stat.S_IMODE(mode)
38
+ if stat.S_ISLNK(mode):
39
+ return ("symlink", permissions, os.readlink(path))
40
+ if stat.S_ISREG(mode):
41
+ return ("file", permissions, path.read_bytes())
42
+ if stat.S_ISDIR(mode):
43
+ return (
44
+ "directory",
45
+ permissions,
46
+ tuple((child.name, snapshot(child)) for child in sorted(path.iterdir())),
47
+ )
48
+ return ("special", permissions)
49
+
50
+
51
+ class ProvisionTransactionTests(unittest.TestCase):
52
+ def setUp(self) -> None:
53
+ source = os.environ.get("PJ_BMAD_PACK_ROOT")
54
+ if not source:
55
+ self.skipTest("PJ_BMAD_PACK_ROOT must identify the canonical candidate pack")
56
+ self.temporary = tempfile.TemporaryDirectory(prefix="commonproject-bmad-transaction-")
57
+ self.root = Path(self.temporary.name)
58
+ self.pack = self.root / "pack"
59
+ shutil.copytree(source, self.pack, symlinks=True)
60
+ self.project = self.root / "project"
61
+ skills = self.project / ".agents" / "skills"
62
+ skills.mkdir(parents=True)
63
+ manifest = self.project / ".agents" / "skills.json"
64
+ private = skills / "bmad-private-custom"
65
+ private.mkdir()
66
+ (private / "SKILL.md").write_text("private custom must survive\n")
67
+ manifest.write_text(
68
+ json.dumps(
69
+ {
70
+ "skills": [
71
+ {"name": "custom", "source": "file:///custom"},
72
+ {"name": "bmad-private-custom", "source": private.as_uri()},
73
+ {"name": "bmad-stale", "source": (self.pack / "bmad-stale").as_uri()},
74
+ ]
75
+ }
76
+ )
77
+ + "\n"
78
+ )
79
+ manifest.chmod(0o600)
80
+ copied = skills / "bmad-agent-pm"
81
+ copied.mkdir()
82
+ (copied / "legacy.txt").write_text("preserve exactly\n")
83
+ (skills / "bmad-agent-analyst").symlink_to(
84
+ self.pack / "bmad-agent-analyst", target_is_directory=True
85
+ )
86
+ (skills / "bmad-stale").symlink_to(
87
+ self.pack / "bmad-stale", target_is_directory=True
88
+ )
89
+ custom = skills / "custom"
90
+ custom.mkdir()
91
+ (custom / "SKILL.md").write_text("custom\n")
92
+ self.previous_pack = os.environ.get("PJ_BMAD_PACK_ROOT")
93
+ os.environ["PJ_BMAD_PACK_ROOT"] = str(self.pack)
94
+
95
+ def tearDown(self) -> None:
96
+ if self.previous_pack is None:
97
+ os.environ.pop("PJ_BMAD_PACK_ROOT", None)
98
+ else:
99
+ os.environ["PJ_BMAD_PACK_ROOT"] = self.previous_pack
100
+ self.temporary.cleanup()
101
+
102
+ def test_nth_link_failure_restores_exact_project_state(self) -> None:
103
+ before = snapshot(self.project)
104
+
105
+ def fail_fifth(target: Path, link: Path, index: int) -> None:
106
+ if index == 5:
107
+ raise OSError("injected fifth-link failure")
108
+ link.symlink_to(target, target_is_directory=True)
109
+
110
+ with working_directory(self.project):
111
+ with self.assertRaisesRegex(OSError, "fifth-link"):
112
+ PROVISIONER.provision(create_link=fail_fifth)
113
+
114
+ self.assertEqual(snapshot(self.project), before)
115
+
116
+ def test_pack_mutation_after_preflight_rolls_back_exactly(self) -> None:
117
+ before = snapshot(self.project)
118
+
119
+ def mutate_pack() -> None:
120
+ (self.pack / "bmad-agent-pm" / "SKILL.md").write_text("mutated after preflight\n")
121
+
122
+ with working_directory(self.project):
123
+ with self.assertRaisesRegex(ValueError, "digest mismatch"):
124
+ PROVISIONER.provision(after_preflight=mutate_pack)
125
+
126
+ self.assertEqual(snapshot(self.project), before)
127
+
128
+ def test_malformed_manifest_fails_before_creating_skills_or_temp_paths(self) -> None:
129
+ project = self.root / "malformed-project"
130
+ agents = project / ".agents"
131
+ agents.mkdir(parents=True)
132
+ manifest = agents / "skills.json"
133
+ manifest.write_bytes(b"{malformed\n")
134
+ manifest.chmod(0o600)
135
+ before = snapshot(project)
136
+
137
+ with working_directory(project):
138
+ with self.assertRaises(json.JSONDecodeError):
139
+ PROVISIONER.provision()
140
+
141
+ self.assertEqual(snapshot(project), before)
142
+ self.assertFalse((agents / "skills").exists())
143
+ self.assertFalse(any(path.name.startswith(".bmad-transaction-") for path in agents.iterdir()))
144
+
145
+ def test_applied_projection_mismatch_rolls_back_exactly(self) -> None:
146
+ before = snapshot(self.project)
147
+
148
+ def corrupt_projection(_manifest: Path, skills: Path) -> None:
149
+ link = skills / "bmad-agent-analyst"
150
+ link.unlink()
151
+ link.symlink_to("/tmp/wrong-after-apply", target_is_directory=True)
152
+
153
+ with working_directory(self.project):
154
+ with self.assertRaisesRegex(ValueError, "link differs from plan"):
155
+ PROVISIONER.provision(after_apply=corrupt_projection)
156
+
157
+ self.assertEqual(snapshot(self.project), before)
158
+
159
+ def test_success_is_complete_and_idempotent(self) -> None:
160
+ with working_directory(self.project):
161
+ self.assertGreater(PROVISIONER.provision(), 0)
162
+ after_first = snapshot(self.project)
163
+ self.assertEqual(PROVISIONER.provision(), 0)
164
+
165
+ self.assertEqual(snapshot(self.project), after_first)
166
+ skills = self.project / ".agents" / "skills"
167
+ self.assertTrue((skills / "bmad-private-custom").is_dir())
168
+ self.assertEqual(
169
+ (skills / "bmad-private-custom" / "SKILL.md").read_text(),
170
+ "private custom must survive\n",
171
+ )
172
+ self.assertFalse((skills / "bmad-stale").exists())
173
+ manifest = json.loads((self.project / ".agents" / "skills.json").read_text())
174
+ self.assertIn(
175
+ {
176
+ "name": "bmad-private-custom",
177
+ "source": (skills / "bmad-private-custom").as_uri(),
178
+ },
179
+ manifest["skills"],
180
+ )
181
+ self.assertNotIn("bmad-stale", [entry.get("name") for entry in manifest["skills"]])
182
+ self.assertTrue(
183
+ all(
184
+ (skills / path.name).is_symlink()
185
+ for path in PROVISIONER.validate_trusted_pack(self.pack)
186
+ )
187
+ )
188
+
189
+ def test_unowned_bmad_prefixed_custom_skill_is_preserved(self) -> None:
190
+ custom = self.project / ".agents" / "skills" / "bmad-private-custom"
191
+ before = snapshot(custom)
192
+
193
+ with working_directory(self.project):
194
+ self.assertGreater(PROVISIONER.provision(), 0)
195
+ after_first = snapshot(self.project)
196
+ self.assertEqual(PROVISIONER.provision(), 0)
197
+
198
+ self.assertEqual(snapshot(custom), before)
199
+ self.assertEqual(snapshot(self.project), after_first)
200
+
201
+
202
+ if __name__ == "__main__":
203
+ unittest.main()
@@ -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"