@delorenj/pjangler 1.4.6 → 1.5.2
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/README.md +41 -596
- package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
- package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
- package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
- package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
- package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
- package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
- package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
- package/dist/index.js +9385 -18379
- package/dist/mcp-server.js +9432 -17852
- package/dist/notebook/journal-service-main.js +5682 -0
- package/dist/project-registry-service.js +748 -0
- package/dist/prompt.js +2 -1
- package/package.json +15 -10
- package/scripts/install-project-registry.mjs +34 -0
- package/scripts/migrate-project-registry.mjs +117 -0
- package/src/project/registryClient.ts +62 -0
- package/templates/commonproject/copier.yml +16 -19
- package/templates/commonproject/template/.agents/hooks/README.md +10 -4
- package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
- package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
- package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
- package/templates/commonproject/template/.agents/skills.json +0 -1
- package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
- package/templates/commonproject/template/.project.json.jinja +1 -1
- package/templates/commonproject/template/mise.toml.jinja +11 -21
- package/contracts/fleet-contract.yaml +0 -584
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
- package/templates/hermes-agent/copier.yml +0 -186
- package/templates/hermes-agent/template/.gitignore.jinja +0 -11
- package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
- package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
- package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
- package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
- package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
- package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
- package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
- package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
- package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
- package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
- package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
- package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
- package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
- package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
- package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
- package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
- package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
- package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
- package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
- package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
- package/templates/hermes-agent/template/hermes.jinja +0 -109
- package/templates/hermes-agent/template/momo.jinja +0 -177
- package/templates/hermes-agent/template/role.yaml.jinja +0 -112
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
dba418f3b40d2e1362e615bdfda6cb1616ec5e49e2e8bd81ca48144ef219b29f agents/openai.yaml
|
|
2
2
|
8a469caf88ed05dc466ffaad611cb45dd0c26046595d9538415d7c89287c8800 hooks/claude.settings.json
|
|
3
3
|
8a469caf88ed05dc466ffaad611cb45dd0c26046595d9538415d7c89287c8800 hooks/hooks.master.json
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
bb37fa22701c93bc02afde2b54be7c34ebc939e52628f4b13f40d9aa55591486 hooks/session-end.sh
|
|
5
|
+
dca8e974513fd3ddaba3d94cd0386f4ac277f548b2af583c7f96b86270655662 hooks/session-start.sh
|
|
6
|
+
70bc3e9b084ef4f2e760987dea1f1cf97d85c6e4ad6312853fd4b9281c57184e references/configuration.md
|
|
7
7
|
6a61d9dd0d5dd32f8a7fefcb82001bbad90523704fa1500b7e0becc3a848545c references/recovery.md
|
|
8
|
-
|
|
8
|
+
62b15b5de6a67f2cf26d279119e1160ea82e31658446af660727643c76f9f3f4 scripts/project-hooks.py
|
|
9
9
|
8567ab2567ec412eb8d673dd713340b81a45e69374d9baee3cb7e827999be437 SKILL.md
|
|
10
|
-
|
|
10
|
+
d4dca28cb7074edce3cf1e13b85e9cd57b6509c2726a561a5d02ea8e36ca1028 tests/test_project_hooks.py
|
|
@@ -19,17 +19,17 @@
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"path": "hooks/session-end.sh",
|
|
22
|
-
"sha256": "
|
|
22
|
+
"sha256": "bb37fa22701c93bc02afde2b54be7c34ebc939e52628f4b13f40d9aa55591486",
|
|
23
23
|
"mode": "0755"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "hooks/session-start.sh",
|
|
27
|
-
"sha256": "
|
|
27
|
+
"sha256": "dca8e974513fd3ddaba3d94cd0386f4ac277f548b2af583c7f96b86270655662",
|
|
28
28
|
"mode": "0755"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "references/configuration.md",
|
|
32
|
-
"sha256": "
|
|
32
|
+
"sha256": "70bc3e9b084ef4f2e760987dea1f1cf97d85c6e4ad6312853fd4b9281c57184e",
|
|
33
33
|
"mode": "0644"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"path": "scripts/project-hooks.py",
|
|
42
|
-
"sha256": "
|
|
42
|
+
"sha256": "62b15b5de6a67f2cf26d279119e1160ea82e31658446af660727643c76f9f3f4",
|
|
43
43
|
"mode": "0755"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"path": "tests/test_project_hooks.py",
|
|
52
|
-
"sha256": "
|
|
52
|
+
"sha256": "d4dca28cb7074edce3cf1e13b85e9cd57b6509c2726a561a5d02ea8e36ca1028",
|
|
53
53
|
"mode": "0644"
|
|
54
54
|
}
|
|
55
55
|
]
|
|
@@ -16,7 +16,7 @@ DISPLAY_EVENT = "SessionEnd"
|
|
|
16
16
|
HOOK_EVENT = "session-close"
|
|
17
17
|
REQUEST_LIMIT_BYTES = 1_048_576
|
|
18
18
|
STREAM_LIMIT_BYTES = REQUEST_LIMIT_BYTES + 1
|
|
19
|
-
CHILD_TIMEOUT_SECONDS = 0
|
|
19
|
+
CHILD_TIMEOUT_SECONDS = 4.0
|
|
20
20
|
NODE_BINARY = Path("/usr/bin/node")
|
|
21
21
|
AUTH_VARIABLE = "OPEN_NOTEBOOK_PASSWORD"
|
|
22
22
|
|
|
@@ -208,8 +208,27 @@ def invoke(entry: pwd.struct_passwd, launcher: Path, payload: bytes) -> int:
|
|
|
208
208
|
return 0
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
def hub_owned(home: Path) -> bool:
|
|
212
|
+
if os.environ.get("PJ_HOOK_FROM_HUB") == "1":
|
|
213
|
+
return False
|
|
214
|
+
if os.environ.get("BB_HOOK_HUB") == "off":
|
|
215
|
+
return False
|
|
216
|
+
helper = home / ".agents/hooks/hub/ownership.py"
|
|
217
|
+
if not helper.is_file():
|
|
218
|
+
return False
|
|
219
|
+
try:
|
|
220
|
+
return subprocess.run(
|
|
221
|
+
[sys.executable, "-I", str(helper), "project-notebook-end", "--cli", "claude"],
|
|
222
|
+
capture_output=True, timeout=0.5, check=False,
|
|
223
|
+
).returncode == 0
|
|
224
|
+
except (OSError, subprocess.SubprocessError):
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
211
228
|
def main() -> int:
|
|
212
229
|
entry = canonical_identity()
|
|
230
|
+
if hub_owned(Path(entry.pw_dir)):
|
|
231
|
+
return 0
|
|
213
232
|
validate_node_binary()
|
|
214
233
|
launcher = resolve_launcher(entry)
|
|
215
234
|
payload = sys.stdin.buffer.read(STREAM_LIMIT_BYTES)
|
|
@@ -16,7 +16,7 @@ DISPLAY_EVENT = "SessionStart"
|
|
|
16
16
|
HOOK_EVENT = "session-start"
|
|
17
17
|
REQUEST_LIMIT_BYTES = 1_048_576
|
|
18
18
|
STREAM_LIMIT_BYTES = REQUEST_LIMIT_BYTES + 1
|
|
19
|
-
CHILD_TIMEOUT_SECONDS =
|
|
19
|
+
CHILD_TIMEOUT_SECONDS = 4.0
|
|
20
20
|
NODE_BINARY = Path("/usr/bin/node")
|
|
21
21
|
AUTH_VARIABLE = "OPEN_NOTEBOOK_PASSWORD"
|
|
22
22
|
|
|
@@ -208,8 +208,27 @@ def invoke(entry: pwd.struct_passwd, launcher: Path, payload: bytes) -> int:
|
|
|
208
208
|
return 0
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
def hub_owned(home: Path) -> bool:
|
|
212
|
+
if os.environ.get("PJ_HOOK_FROM_HUB") == "1":
|
|
213
|
+
return False
|
|
214
|
+
if os.environ.get("BB_HOOK_HUB") == "off":
|
|
215
|
+
return False
|
|
216
|
+
helper = home / ".agents/hooks/hub/ownership.py"
|
|
217
|
+
if not helper.is_file():
|
|
218
|
+
return False
|
|
219
|
+
try:
|
|
220
|
+
return subprocess.run(
|
|
221
|
+
[sys.executable, "-I", str(helper), "project-notebook-start", "--cli", "claude"],
|
|
222
|
+
capture_output=True, timeout=0.5, check=False,
|
|
223
|
+
).returncode == 0
|
|
224
|
+
except (OSError, subprocess.SubprocessError):
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
211
228
|
def main() -> int:
|
|
212
229
|
entry = canonical_identity()
|
|
230
|
+
if hub_owned(Path(entry.pw_dir)):
|
|
231
|
+
return 0
|
|
213
232
|
validate_node_binary()
|
|
214
233
|
launcher = resolve_launcher(entry)
|
|
215
234
|
payload = sys.stdin.buffer.read(STREAM_LIMIT_BYTES)
|
|
@@ -91,3 +91,5 @@ the 1,048,576-byte request ceiling was exceeded. Valid-size input is sent over
|
|
|
91
91
|
stdin with an explicit child timeout shorter than the outer hook timeout.
|
|
92
92
|
Resolution, validation, input, timeout, and PJangler failures are bounded and
|
|
93
93
|
fail open without creating wrapper-local state.
|
|
94
|
+
|
|
95
|
+
When the Bloodbank hub owns `project-notebook-start` or `project-notebook-end`, installation removes only that event’s native notebook copy and `check` accepts the centralized owner. A paused hub concern stays owned. Without hub ownership, the standalone Claude projection remains available. Legacy wrappers also defer to the central owner, while supervised hub children execute normally.
|
|
@@ -14,6 +14,7 @@ import posixpath
|
|
|
14
14
|
import secrets
|
|
15
15
|
import shlex
|
|
16
16
|
import stat
|
|
17
|
+
import subprocess
|
|
17
18
|
import sys
|
|
18
19
|
from collections.abc import Iterator, Sequence
|
|
19
20
|
from dataclasses import dataclass
|
|
@@ -433,10 +434,32 @@ def _dedicated_owned_group(group: dict[str, Any], event: str) -> bool:
|
|
|
433
434
|
)
|
|
434
435
|
|
|
435
436
|
|
|
437
|
+
def _hub_owned(event: str) -> bool:
|
|
438
|
+
"""The central registry remains the owner while a concern is paused."""
|
|
439
|
+
if os.environ.get("BB_HOOK_HUB") == "off":
|
|
440
|
+
return False
|
|
441
|
+
helper = Path.home() / ".agents/hooks/hub/ownership.py"
|
|
442
|
+
if not helper.is_file():
|
|
443
|
+
return False
|
|
444
|
+
concern = "project-notebook-start" if event == "SessionStart" else "project-notebook-end"
|
|
445
|
+
try:
|
|
446
|
+
return subprocess.run(
|
|
447
|
+
[sys.executable, str(helper), concern, "--cli", "claude"],
|
|
448
|
+
capture_output=True, timeout=1, check=False,
|
|
449
|
+
).returncode == 0
|
|
450
|
+
except (OSError, subprocess.SubprocessError):
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
|
|
436
454
|
def _install_projection(live: dict[str, Any], fragment: dict[str, Any]) -> dict[str, Any]:
|
|
437
|
-
|
|
455
|
+
owned_events = {event for event in EVENT_ORDER if _hub_owned(event)}
|
|
456
|
+
merged = _uninstall_projection(live, owned_events)
|
|
457
|
+
if len(owned_events) == len(EVENT_ORDER):
|
|
458
|
+
return merged
|
|
438
459
|
hooks = merged.setdefault("hooks", {})
|
|
439
460
|
for event in EVENT_ORDER:
|
|
461
|
+
if event in owned_events:
|
|
462
|
+
continue
|
|
440
463
|
canonical_group = fragment["hooks"][event][0]
|
|
441
464
|
canonical_hook = canonical_group["hooks"][0]
|
|
442
465
|
groups = hooks.setdefault(event, [])
|
|
@@ -469,12 +492,14 @@ def _install_projection(live: dict[str, Any], fragment: dict[str, Any]) -> dict[
|
|
|
469
492
|
return merged
|
|
470
493
|
|
|
471
494
|
|
|
472
|
-
def _uninstall_projection(
|
|
495
|
+
def _uninstall_projection(
|
|
496
|
+
live: dict[str, Any], events: Sequence[str] = EVENT_ORDER
|
|
497
|
+
) -> dict[str, Any]:
|
|
473
498
|
merged = copy.deepcopy(live)
|
|
474
499
|
hooks = merged.get("hooks")
|
|
475
500
|
if not isinstance(hooks, dict):
|
|
476
501
|
return merged
|
|
477
|
-
for event in
|
|
502
|
+
for event in events:
|
|
478
503
|
groups = hooks.get(event)
|
|
479
504
|
if not isinstance(groups, list):
|
|
480
505
|
continue
|
|
@@ -528,6 +553,11 @@ def _check_findings(live: dict[str, Any], fragment: dict[str, Any]) -> list[dict
|
|
|
528
553
|
|
|
529
554
|
for event in EVENT_ORDER:
|
|
530
555
|
entries = owned[event]
|
|
556
|
+
if _hub_owned(event):
|
|
557
|
+
if entries:
|
|
558
|
+
findings.append({"kind": "duplicate", "event": event, "count": len(entries),
|
|
559
|
+
"message": "central hook hub owns this concern; remove native copies"})
|
|
560
|
+
continue
|
|
531
561
|
if not entries:
|
|
532
562
|
findings.append(
|
|
533
563
|
{"kind": "missing", "event": event, "message": "canonical hook is absent"}
|
|
@@ -49,6 +49,12 @@ class ProjectHooksTests(unittest.TestCase):
|
|
|
49
49
|
self.temporary = tempfile.TemporaryDirectory()
|
|
50
50
|
self.addCleanup(self.temporary.cleanup)
|
|
51
51
|
self.root = Path(self.temporary.name)
|
|
52
|
+
environment = mock.patch.dict(os.environ, {
|
|
53
|
+
"BB_HOOK_OWNERSHIP": str(self.root / "uninstalled-ownership.json"),
|
|
54
|
+
"BB_HOOK_HUB": "",
|
|
55
|
+
})
|
|
56
|
+
environment.start()
|
|
57
|
+
self.addCleanup(environment.stop)
|
|
52
58
|
self.claude = self.root / ".claude"
|
|
53
59
|
self.claude.mkdir(mode=0o700)
|
|
54
60
|
self.target = self.claude / "settings.json"
|
|
@@ -193,6 +199,37 @@ class ProjectHooksTests(unittest.TestCase):
|
|
|
193
199
|
env=environment,
|
|
194
200
|
)
|
|
195
201
|
|
|
202
|
+
def test_hub_owned_install_retires_only_native_notebook_copies(self) -> None:
|
|
203
|
+
foreign = {"matcher": "preserved", "hooks": [{"command": "foreign-command"}]}
|
|
204
|
+
live = {"theme": "dark", "hooks": {
|
|
205
|
+
"SessionStart": [copy.deepcopy(foreign), *self.master["hooks"]["SessionStart"]],
|
|
206
|
+
"SessionEnd": copy.deepcopy(self.master["hooks"]["SessionEnd"]),
|
|
207
|
+
}}
|
|
208
|
+
with mock.patch.object(PROJECTOR, "_hub_owned", return_value=True):
|
|
209
|
+
projected = PROJECTOR._install_projection(live, self.master)
|
|
210
|
+
self.assertEqual(projected, {"theme": "dark", "hooks": {"SessionStart": [foreign]}})
|
|
211
|
+
self.assertEqual(PROJECTOR._install_projection(projected, self.master), projected)
|
|
212
|
+
self.assertEqual(PROJECTOR._check_findings(projected, self.master), [])
|
|
213
|
+
self.assertEqual({row["kind"] for row in PROJECTOR._check_findings(live, self.master)}, {"duplicate"})
|
|
214
|
+
|
|
215
|
+
def test_hub_owned_one_event_preserves_other_standalone_fallback(self) -> None:
|
|
216
|
+
with mock.patch.object(PROJECTOR, "_hub_owned", side_effect=lambda event: event == "SessionStart"):
|
|
217
|
+
projected = PROJECTOR._install_projection({}, self.master)
|
|
218
|
+
self.assertEqual(projected, {"hooks": {"SessionEnd": self.master["hooks"]["SessionEnd"]}})
|
|
219
|
+
|
|
220
|
+
def test_legacy_wrapper_defers_to_hub_but_hub_child_runs(self) -> None:
|
|
221
|
+
fake_home, _ = self.make_fake_pj()
|
|
222
|
+
helper = fake_home / ".agents/hooks/hub/ownership.py"
|
|
223
|
+
helper.parent.mkdir(parents=True)
|
|
224
|
+
helper.write_text("raise SystemExit(0)\n")
|
|
225
|
+
for wrapper in ("session-start.sh", "session-end.sh"):
|
|
226
|
+
native = self.run_wrapper(wrapper, fake_home, b"{}")
|
|
227
|
+
self.assertEqual(native.returncode, 0, native.stderr)
|
|
228
|
+
self.assertEqual(native.stdout, b"")
|
|
229
|
+
supervised = self.run_wrapper(wrapper, fake_home, b"{}", extra_environment={"BB_HOOK_HUB": "off"})
|
|
230
|
+
self.assertEqual(supervised.returncode, 0, supervised.stderr)
|
|
231
|
+
self.assertTrue(supervised.stdout, supervised.stderr)
|
|
232
|
+
|
|
196
233
|
def test_master_fragment_and_wrappers_are_exact(self) -> None:
|
|
197
234
|
self.assertEqual(MASTER_PATH.read_bytes(), FRAGMENT_PATH.read_bytes())
|
|
198
235
|
self.assertEqual(list(self.master["hooks"]), ["SessionStart", "SessionEnd"])
|