@delorenj/pjangler 1.4.5 → 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.
Files changed (83) hide show
  1. package/README.md +41 -520
  2. package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
  3. package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
  4. package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
  5. package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
  6. package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
  7. package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
  8. package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
  9. package/dist/index.js +5747 -13483
  10. package/dist/mcp-server.js +6853 -14059
  11. package/dist/notebook/journal-service-main.js +5682 -0
  12. package/dist/project-registry-service.js +748 -0
  13. package/dist/prompt.js +2 -1
  14. package/package.json +15 -10
  15. package/scripts/install-project-registry.mjs +34 -0
  16. package/scripts/migrate-project-registry.mjs +117 -0
  17. package/src/project/registryClient.ts +62 -0
  18. package/templates/commonproject/copier.yml +16 -19
  19. package/templates/commonproject/template/.agents/hooks/README.md +10 -4
  20. package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
  21. package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
  22. package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
  23. package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
  24. package/templates/commonproject/template/.agents/skills.json +0 -1
  25. package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
  26. package/templates/commonproject/template/.project.json.jinja +1 -1
  27. package/templates/commonproject/template/mise.toml.jinja +11 -21
  28. package/contracts/fleet-contract.yaml +0 -513
  29. package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
  30. package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
  31. package/templates/hermes-agent/copier.yml +0 -186
  32. package/templates/hermes-agent/template/.gitignore.jinja +0 -11
  33. package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
  34. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
  35. package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
  36. package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
  37. package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
  38. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
  39. package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
  40. package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
  41. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
  42. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
  43. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
  44. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
  45. package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
  46. package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
  47. package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
  48. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
  49. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
  50. package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
  51. package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
  52. package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
  53. package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
  54. package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
  55. package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
  56. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
  57. package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
  58. package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
  59. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
  60. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
  61. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
  62. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
  63. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
  64. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
  65. package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
  66. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
  67. package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
  68. package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
  69. package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
  70. package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
  71. package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
  72. package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
  73. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
  74. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
  75. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
  76. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
  77. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
  78. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
  79. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
  80. package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
  81. package/templates/hermes-agent/template/hermes.jinja +0 -109
  82. package/templates/hermes-agent/template/momo.jinja +0 -177
  83. package/templates/hermes-agent/template/role.yaml.jinja +0 -112
@@ -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.75
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 = 2.25
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
- merged = copy.deepcopy(live)
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(live: dict[str, Any]) -> dict[str, Any]:
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 EVENT_ORDER:
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"])