@delorenj/pjangler 1.3.0 → 1.4.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 (43) hide show
  1. package/.mise/scripts/link-agentfiles.sh +38 -5
  2. package/README.md +92 -0
  3. package/dist/assets/project-notebook-skill/SHA256SUMS +10 -0
  4. package/dist/assets/project-notebook-skill/SKILL.md +64 -0
  5. package/dist/assets/project-notebook-skill/agents/openai.yaml +6 -0
  6. package/dist/assets/project-notebook-skill/export-manifest.json +56 -0
  7. package/dist/assets/project-notebook-skill/hooks/claude.settings.json +26 -0
  8. package/dist/assets/project-notebook-skill/hooks/hooks.master.json +26 -0
  9. package/dist/assets/project-notebook-skill/hooks/session-end.sh +228 -0
  10. package/dist/assets/project-notebook-skill/hooks/session-start.sh +228 -0
  11. package/dist/assets/project-notebook-skill/references/configuration.md +93 -0
  12. package/dist/assets/project-notebook-skill/references/recovery.md +54 -0
  13. package/dist/assets/project-notebook-skill/scripts/project-hooks.py +865 -0
  14. package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +848 -0
  15. package/dist/index.js +11307 -2809
  16. package/dist/mcp-server.js +9637 -2094
  17. package/dist/prompt.js +404 -0
  18. package/package.json +8 -5
  19. package/templates/commonproject/copier.yml +19 -5
  20. package/templates/commonproject/template/.mise/scripts/link-agentfiles.sh +38 -5
  21. package/templates/commonproject/template/.mise/scripts/provision-packs.py +74 -52
  22. package/templates/commonproject/template/.mise/scripts/sync-skills.py +479 -24
  23. package/templates/commonproject/template/mise.toml.jinja +12 -6
  24. package/templates/hermes-agent/copier.yml +8 -11
  25. package/templates/hermes-agent/template/.gitignore.jinja +1 -0
  26. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
  27. package/templates/hermes-agent/template/.scripts/01-config.sh +9 -0
  28. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
  29. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +68 -4
  30. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
  31. package/templates/hermes-agent/template/.scripts/70-systemd.sh +77 -43
  32. package/templates/hermes-agent/template/.scripts/80-registry.sh +6 -0
  33. package/templates/hermes-agent/template/.scripts/_lib.sh +116 -16
  34. package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
  35. package/templates/hermes-agent/template/.scripts/heartbeat.sh +13 -1
  36. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
  37. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +761 -0
  38. package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
  39. package/templates/hermes-agent/template/.scripts/providers/plane.sh +19 -3
  40. package/templates/hermes-agent/template/SOUL.md.jinja +44 -8
  41. package/templates/hermes-agent/template/hermes.jinja +20 -8
  42. package/templates/hermes-agent/template/momo.jinja +177 -0
  43. package/templates/hermes-agent/template/role.yaml.jinja +19 -19
@@ -19,15 +19,17 @@ What this script owns is the transactional projection into the project:
19
19
  one unsafe or tampered pack produces ZERO mutation
20
20
  * `.agents/skills.json` is rewritten atomically, preserving its mode
21
21
 
22
- Backwards compatibility: a project that declares no `bmad` pack still gets the
23
- pinned BMAD pack expanded into `skills[]`, exactly as before. Declaring
24
- `{"name": "bmad", ...}` in `packs[]` takes over and the pin is not consulted.
22
+ Only packs a project DECLARES in `packs[]` are provisioned. Nothing is pinned
23
+ implicitly -- in particular BMAD is not a pack: `bmad-method install` writes
24
+ bmad-* skills into `.agents/skills` itself, versioned per project by
25
+ `_bmad/_config/manifest.yaml`.
25
26
  """
26
27
 
27
28
  from __future__ import annotations
28
29
 
29
30
  import importlib.util
30
31
  import json
32
+ import argparse
31
33
  import os
32
34
  import re
33
35
  import shutil
@@ -40,11 +42,12 @@ from typing import Callable
40
42
  SKILLS_SCHEMA = "https://raw.githubusercontent.com/delorenj/skillex/main/skills.schema.json"
41
43
  SKILLS_REGISTRY = "https://github.com/delorenj/skillex.git"
42
44
 
43
- # The BMAD pack pinned when a project declares none. Sealed from this side so
44
- # the pinned release is verified byte-for-byte even though its `pack.toml`
45
- # predates `[policy] sealed`.
46
- BMAD_PACK_NAME = "bmad"
47
- BMAD_PACK_VERSION = "6.10.1-next.31"
45
+ # BMAD is NOT a pack. `bmad-method install` writes bmad-* skills into
46
+ # .agents/skills itself, per project, versioned by _bmad/_config/manifest.yaml.
47
+ # This script used to pin a frozen `packs/bmad/<version>` in the registry and
48
+ # project a second copy of the same skills; when the registry dropped that pack
49
+ # the pin took every `pjangler project create` down with it on any machine
50
+ # without a warm cache. Only packs a project DECLARES are provisioned now.
48
51
 
49
52
 
50
53
  def load_engine():
@@ -68,38 +71,13 @@ engine = load_engine()
68
71
 
69
72
 
70
73
  def pack_root_override(name: str) -> Path | None:
71
- """Developer/test pin for one pack root, e.g. `PJ_PACK_ROOT_HERMES_BASE`.
72
-
73
- `PJ_BMAD_PACK_ROOT` predates the generic form and stays a first-class alias
74
- for the `bmad` pack.
75
- """
74
+ """Developer/test pin for one pack root, e.g. `PJ_PACK_ROOT_HERMES_BASE`."""
76
75
  generic = os.environ.get(f"PJ_PACK_ROOT_{re.sub(r'[^A-Z0-9]', '_', name.upper())}", "").strip()
77
76
  if generic:
78
77
  return Path(generic).expanduser().absolute()
79
- if name == BMAD_PACK_NAME:
80
- legacy = os.environ.get("PJ_BMAD_PACK_ROOT", "").strip()
81
- if legacy:
82
- return Path(legacy).expanduser().absolute()
83
78
  return None
84
79
 
85
80
 
86
- def implicit_bmad_entry() -> dict:
87
- """The implicit BMAD pin, expressed as an ordinary `packs[]` entry.
88
-
89
- It deliberately carries NO `source`: like every declared pack it must walk
90
- the one resolution ladder in `resolve_pack_root()` -- env override first,
91
- then the contract-ordered registry checkouts. Pinning a hardcoded root here
92
- would give the same pack name two resolutions in one process, so adding
93
- `packs:[{"name":"bmad",...}]` to a manifest would silently MOVE the pack.
94
- """
95
- return {
96
- "name": BMAD_PACK_NAME,
97
- "version": BMAD_PACK_VERSION,
98
- "sealed": True,
99
- "optional": False,
100
- }
101
-
102
-
103
81
  def apply_root_override(entry: dict) -> dict:
104
82
  """Pin a declared pack at an overridden root without changing its identity."""
105
83
  if entry.get("source") or entry.get("registry_path"):
@@ -150,24 +128,11 @@ def resolve_declared_packs(manifest: dict, base_dir: Path):
150
128
  # Later packs override earlier ones (contract section 5).
151
129
  declared_members[name] = path
152
130
 
131
+ # Nothing is pinned implicitly any more. The empty pair is kept so every
132
+ # caller keeps one shape and the manifest writer still evicts leftovers
133
+ # from when something WAS pinned here.
153
134
  implicit_members: dict[str, Path] = {}
154
135
  implicit_packs: list[dict] = []
155
- if not any(entry["name"] == BMAD_PACK_NAME for entry in entries):
156
- # Same call shape as a declared pack above, so the pin cannot resolve
157
- # anywhere a declared `bmad` entry would not.
158
- pinned = engine.normalize_pack_entry(apply_root_override(implicit_bmad_entry()))
159
- pinned["sealed"] = True
160
- for name, path in engine.resolve_pack(
161
- pinned,
162
- cache_dir,
163
- base_dir,
164
- default_registry,
165
- registry_roots,
166
- managed_roots,
167
- on_resolved=implicit_packs.append,
168
- ):
169
- implicit_members[name] = path
170
-
171
136
  return declared_members, implicit_members, declared_packs, implicit_packs
172
137
 
173
138
 
@@ -322,13 +287,46 @@ def atomic_write(path: Path, content: bytes, mode: int) -> None:
322
287
  # --------------------------------------------------------------------------- #
323
288
 
324
289
 
290
+ def own_root() -> Path:
291
+ """The repo this script file belongs to: <root>/.mise/scripts/<this>."""
292
+ return Path(__file__).resolve().parents[2]
293
+
294
+
295
+ def resolved_project_root(requested: str | None) -> Path:
296
+ """Resolve the repo to provision, refusing to infer it from cwd.
297
+
298
+ A mise ENTER hook runs with cwd set to the directory the user cd'd into, not
299
+ config_root -- including a PARENT config's hook. That is how 33GOD's copy of
300
+ this script came to force-rewrite `pjangler/.agents/skills.json` and plant 75
301
+ dangling pack links in `pjangler/.agents/skills`. `mise run <task>` does run
302
+ at config_root, so the cwd assumption was only ever wrong on the hook path.
303
+ """
304
+ mine = own_root()
305
+ if requested is None:
306
+ requested = os.environ.get("MISE_CONFIG_ROOT") or None
307
+ if requested is None:
308
+ raise SystemExit(
309
+ "provision-packs: --root (or $MISE_CONFIG_ROOT) is required; "
310
+ "refusing to infer the subject repo from cwd"
311
+ )
312
+ resolved = Path(requested).resolve(strict=True)
313
+ if resolved != mine:
314
+ raise SystemExit(
315
+ f"provision-packs: refusing to act on {resolved}; this script "
316
+ f"belongs to {mine}. A nested repo must ship its own "
317
+ ".mise/scripts copy."
318
+ )
319
+ return resolved
320
+
321
+
325
322
  def provision(
326
323
  *,
324
+ root: str | None = None,
327
325
  after_preflight: Callable[[], None] | None = None,
328
326
  create_link: Callable[[Path, Path, int], None] | None = None,
329
327
  after_apply: Callable[[Path, Path], None] | None = None,
330
328
  ) -> int:
331
- project_root = Path.cwd().resolve(strict=True)
329
+ project_root = resolved_project_root(root)
332
330
  agents_path = project_root / ".agents"
333
331
  skills_path = agents_path / "skills"
334
332
  agents_existed = agents_path.exists() or agents_path.is_symlink()
@@ -423,10 +421,24 @@ def provision(
423
421
  )
424
422
  except OSError:
425
423
  link_targets_pack = False
424
+ # PJAN-82: a DANGLING symlink here is stale by definition.
425
+ #
426
+ # An entry stopped being recognizable as pack-managed the moment its pack
427
+ # declaration went away: `ownership_roots` no longer contains the pack, so
428
+ # `link_targets_pack` is false, and the normalized manifest no longer names
429
+ # it either — so the checks below skipped it and it stayed forever. That is
430
+ # how momo, bloodbank and candystore each ended up with 76 links into
431
+ # `~/.agents/.cache/registries/.../packs/bmad/6.10.1-next.31/`, a cache
432
+ # directory that no longer exists. A link that resolves to nothing cannot be
433
+ # a hand-authored skill and cannot be serving anyone, so it is reclaimable
434
+ # regardless of where it points. A link that still RESOLVES is left alone
435
+ # unless it is provably pack-managed.
436
+ dangling_link = link_target is not None and not entry.exists()
426
437
  if (
427
438
  entry.name not in expected
428
439
  and entry.name not in managed_manifest_names
429
440
  and not link_targets_pack
441
+ and not dangling_link
430
442
  ):
431
443
  continue
432
444
  target = expected.get(entry.name)
@@ -550,8 +562,18 @@ def provision(
550
562
 
551
563
 
552
564
  def main() -> None:
565
+ parser = argparse.ArgumentParser(description="Provision declared Skillex packs.")
566
+ parser.add_argument(
567
+ "--root",
568
+ default=None,
569
+ help=(
570
+ "Project root to provision; defaults to $MISE_CONFIG_ROOT. Never "
571
+ "cwd -- see resolved_project_root()."
572
+ ),
573
+ )
574
+ args = parser.parse_args()
553
575
  try:
554
- changed = provision()
576
+ changed = provision(root=args.root)
555
577
  except (FileNotFoundError, OSError, ValueError, RuntimeError, engine.PackUnavailable) as error:
556
578
  raise SystemExit(
557
579
  f"Skillex pack provisioning failed: {error}; "