@delorenj/pjangler 1.3.7 → 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 (27) hide show
  1. package/.mise/scripts/link-agentfiles.sh +38 -5
  2. package/README.md +20 -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 +14320 -7847
  16. package/dist/mcp-server.js +13482 -8146
  17. package/package.json +4 -3
  18. package/templates/commonproject/copier.yml +14 -5
  19. package/templates/commonproject/template/.mise/scripts/link-agentfiles.sh +38 -5
  20. package/templates/commonproject/template/.mise/scripts/provision-packs.py +60 -2
  21. package/templates/commonproject/template/.mise/scripts/sync-skills.py +479 -24
  22. package/templates/commonproject/template/mise.toml.jinja +12 -6
  23. package/templates/hermes-agent/template/.gitignore.jinja +1 -0
  24. package/templates/hermes-agent/template/.scripts/70-systemd.sh +11 -7
  25. package/templates/hermes-agent/template/.scripts/_lib.sh +54 -10
  26. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +29 -2
  27. package/templates/hermes-agent/template/.scripts/providers/plane.sh +18 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.3.7",
3
+ "version": "1.4.2",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "node": ">=20"
24
24
  },
25
25
  "scripts": {
26
- "build": "esbuild src/index.ts src/mcp-server.ts src/prompt.ts --bundle --packages=external --platform=node --format=esm --outdir=dist",
26
+ "build": "esbuild src/index.ts src/mcp-server.ts src/prompt.ts --bundle --packages=external --platform=node --format=esm --outdir=dist && node scripts/export-project-notebook-skill.mjs",
27
27
  "check:audit:prod": "npm audit --omit=dev",
28
28
  "check:lock": "node scripts/check-package-lock-parity.mjs",
29
29
  "check:submodules": "node scripts/check-submodule-contract.mjs",
@@ -32,7 +32,8 @@
32
32
  "mcp": "node dist/mcp-server.js",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "test:bmad-installer-contract": "node tests/bmad-installer-contract-regressions.mjs",
35
- "test": "npm run check:lock && node tests/package-lock-parity-regressions.mjs && npm run check:submodules && npm run check:tracked-secrets && node tests/portable-test-paths-regressions.mjs && node tests/release-regressions.mjs && node tests/submodule-contract-regressions.mjs && node tests/secret-publication-gate-regressions.mjs && node tests/bmad-version-surface-regressions.mjs && node tests/bmad-transaction-regressions.mjs && node tests/parity-migrate-regressions.mjs && node tests/hermes-profile-inheritance-regressions.mjs && node tests/pjan-57-lifecycle-recipes-regressions.mjs && node tests/pjan-57-dogfood-regressions.mjs && node tests/generated-project-lifecycle-regressions.mjs && node tests/pack-flatten-regressions.mjs && node tests/pack-flatten-cross-engine-regressions.mjs && node tests/registry-cache-parity-regressions.mjs && node tests/registry-root-ladder-regressions.mjs && node tests/pjan-23-regressions.mjs && node tests/pjan-24-regressions.mjs && node tests/pjan-28-regressions.mjs && node tests/pjan-30-regressions.mjs && node tests/pjan-31a-regressions.mjs && node tests/pjan-36-regressions.mjs && node tests/pjan-43-regressions.mjs && node tests/pjan-48-regressions.mjs && node tests/pjan-49-regressions.mjs && node tests/pjan-50-regressions.mjs && node tests/pjan-65-regressions.mjs && node tests/pjan-67-lifecycle-preflight-regressions.mjs && node tests/pjan-67-regressions.mjs && node tests/pjan-67-trusted-lifecycle-regressions.mjs && node tests/pjan-71-regressions.mjs && node tests/pjan-72-regressions.mjs && node tests/pjan-75-regressions.mjs && node tests/pjan-76-regressions.mjs && node tests/skillex-init-regressions.mjs && node tests/fleet-shared-bloodbank-regressions.mjs && node tests/mcp-catalog-regressions.mjs && node tests/mcp-server-regressions.mjs && node tests/project-registry-regressions.mjs && node tests/pg-registry-regressions.mjs && node tests/momo-lifecycle-plane-regressions.mjs",
35
+ "test:pjan-77": "node tests/pjan-77-notebook-domain-regressions.mjs && node tests/pjan-77-notebook-adapter-contract.mjs && node tests/pjan-77-notebook-lifecycle-regressions.mjs && node tests/pjan-77-notebook-cli-contract.mjs && node tests/pjan-77-notebook-hooks-capture.mjs && node tests/pjan-77-notebook-security-isolation.mjs && node tests/pjan-77-notebook-release-gates.mjs",
36
+ "test": "npm run check:lock && node tests/package-lock-parity-regressions.mjs && npm run check:submodules && npm run check:tracked-secrets && node tests/portable-test-paths-regressions.mjs && node tests/release-regressions.mjs && node tests/submodule-contract-regressions.mjs && node tests/secret-publication-gate-regressions.mjs && node tests/bmad-version-surface-regressions.mjs && node tests/bmad-transaction-regressions.mjs && node tests/parity-migrate-regressions.mjs && node tests/hermes-profile-inheritance-regressions.mjs && node tests/pjan-57-lifecycle-recipes-regressions.mjs && node tests/pjan-57-dogfood-regressions.mjs && node tests/generated-project-lifecycle-regressions.mjs && node tests/pack-flatten-regressions.mjs && node tests/pack-flatten-cross-engine-regressions.mjs && node tests/registry-cache-parity-regressions.mjs && node tests/registry-root-ladder-regressions.mjs && node tests/pjan-23-regressions.mjs && node tests/pjan-24-regressions.mjs && node tests/pjan-28-regressions.mjs && node tests/pjan-30-regressions.mjs && node tests/pjan-31a-regressions.mjs && node tests/pjan-36-regressions.mjs && node tests/pjan-43-regressions.mjs && node tests/pjan-48-regressions.mjs && node tests/pjan-49-regressions.mjs && node tests/pjan-50-regressions.mjs && node tests/pjan-65-regressions.mjs && node tests/pjan-67-lifecycle-preflight-regressions.mjs && node tests/pjan-67-regressions.mjs && node tests/pjan-67-trusted-lifecycle-regressions.mjs && node tests/pjan-71-regressions.mjs && node tests/pjan-72-regressions.mjs && node tests/pjan-75-regressions.mjs && node tests/pjan-76-regressions.mjs && node tests/skillex-init-regressions.mjs && node tests/pjan-84-global-scope-regressions.mjs && node tests/pjan-84-registry-flag-regressions.mjs && node tests/pjan-84-finding-scope-regressions.mjs && node tests/pjan-84-orphan-adoption-regressions.mjs && node tests/fleet-shared-bloodbank-regressions.mjs && node tests/mcp-catalog-regressions.mjs && node tests/mcp-server-regressions.mjs && node tests/project-registry-regressions.mjs && node tests/pg-registry-regressions.mjs && node tests/momo-lifecycle-plane-regressions.mjs && npm run test:pjan-77",
36
37
  "migrate:up": "node-pg-migrate --migrations-dir migrations up",
37
38
  "migrate:down": "node-pg-migrate --migrations-dir migrations down",
38
39
  "migrate:create": "node-pg-migrate --migrations-dir migrations create",
@@ -114,18 +114,27 @@ _exclude:
114
114
  # into the caller's shared per-user CLI configs. Scaffolding must never mutate
115
115
  # global state as a side effect — the operator trusts/enters the repo themselves.
116
116
  _tasks:
117
- - "cp ~/.config/git/ignore .gitignore 2>/dev/null || echo '# Add ignores here' > .gitignore"
117
+ # PJAN-82: seed the base ignore only when there is nothing to lose. `cp` over
118
+ # an existing .gitignore silently discarded whatever the repo already had,
119
+ # every time the template was re-rendered into a live project.
120
+ - "[ -s .gitignore ] || cp ~/.config/git/ignore .gitignore 2>/dev/null || [ -s .gitignore ] || echo '# Add ignores here' > .gitignore"
118
121
  # Ensure secrets + per-dev agent-hook/skill overrides are ignored (appended after the base ignore).
119
122
  # Each CLI root's hand-owned config is durable project state; the skill
120
123
  # projections inside it are not — bmad-method and skills:sync rewrite them
121
124
  # on every run, so they are re-ignored after the un-ignore lines (last
122
125
  # match wins). _bmad/_config/manifest.yaml pins the version that
123
126
  # reproduces them exactly.
124
- - "printf '\\n# Secrets + per-dev agent-hook/skill overrides (see .agents/local.example.json)\\n.env\\n.env.*\\n!.env.op\\n.agents/local.json\\n.lastagent\\n**/.claude/settings.local.json\\n\\n# Generated CLI configurations are durable project state...\\n!.claude/\\n!.claude/**\\n!.codex/\\n!.codex/**\\n!.gemini/\\n!.gemini/**\\n!.copilot/\\n!.copilot/**\\n!.opencode/\\n!.opencode/**\\n!.kimi-code/\\n!.kimi-code/**\\n# ...but their skill projections are regenerated, so they stay out of the tree.\\n/.agents/skills/\\n.claude/skills/\\n.codex/skills/\\n.gemini/skills/\\n.copilot/skills/\\n.opencode/skills/\\n.kimi-code/skills/\\n' >> .gitignore"
125
- - "ln -sf AGENTS.md CLAUDE.md"
126
- - "ln -sf AGENTS.md GEMINI.md"
127
+ # PJAN-82: appended once (a re-render used to duplicate the whole block), and
128
+ # the skill patterns carry no trailing slash so a SYMLINKED projection is
129
+ # ignored too .gemini/.copilot/.kimi-code get a symlink, not a directory.
130
+ - "grep -q '^# Generated CLI configurations are durable project state' .gitignore 2>/dev/null || printf '\\n# Secrets + per-dev agent-hook/skill overrides (see .agents/local.example.json)\\n.env\\n.env.*\\n!.env.op\\n.agents/local.json\\n.lastagent\\n**/.claude/settings.local.json\\n\\n# Generated CLI configurations are durable project state...\\n!.claude/\\n!.claude/**\\n!.codex/\\n!.codex/**\\n!.gemini/\\n!.gemini/**\\n!.copilot/\\n!.copilot/**\\n!.opencode/\\n!.opencode/**\\n!.kimi-code/\\n!.kimi-code/**\\n# ...but their skill projections are regenerated, so they stay out of the tree.\\n# No trailing slash: some CLIs get a real directory here and some a symlink.\\n/.agents/skills\\n.claude/skills\\n.codex/skills\\n.gemini/skills\\n.copilot/skills\\n.opencode/skills\\n.kimi-code/skills\\n' >> .gitignore"
131
+ # PJAN-82: use the managed script, which refuses to replace a REAL CLAUDE.md.
132
+ # `ln -sf` cannot distinguish a stale link it owns from a hand-written file it
133
+ # must not touch, so a re-render into a live project destroyed the file and
134
+ # reported success.
135
+ - "./.mise/scripts/link-agentfiles.sh \"$(pwd)\""
127
136
  # Materialize the declared Skillex packs' manifest + symlink topology
128
137
  # without copying pack content or disturbing project-specific skills.
129
- - "python3 .mise/scripts/provision-packs.py"
138
+ - "python3 .mise/scripts/provision-packs.py --root \"$(pwd)\""
130
139
  # Stamp the repo's absolute path into .project.json (the SOT) post-render.
131
140
  - "python3 -c \"import json,os,pathlib; p=pathlib.Path('.project.json'); d=json.loads(p.read_text()); d['repo_path']=os.getcwd(); p.write_text(json.dumps(d,indent=2)+chr(10))\""
@@ -1,9 +1,42 @@
1
1
  #!/bin/bash
2
+ set -eu
2
3
 
3
- if [ -f AGENTS.md ]; then
4
- ln -sf AGENTS.md CLAUDE.md
5
- ln -sf AGENTS.md GEMINI.md
6
- echo "✅ AI agent symlinks verified"
4
+ # Symlink the per-CLI agent instruction files at AGENTS.md.
5
+ #
6
+ # A mise ENTER hook runs with cwd set to the directory the user cd'd into, NOT
7
+ # to config_root measured on mise 2026.8.10, and true even for a parent config
8
+ # when the entered directory is a nested child. `mise run <task>` does run at
9
+ # config_root, which is why the old cwd-relative version looked correct for
10
+ # years: only the enter-hook path was wrong. So take the root explicitly, and
11
+ # fall back to this script's own location — never to cwd.
12
+ root="${1:-${MISE_CONFIG_ROOT:-}}"
13
+ own_root=$(CDPATH= cd "$(dirname "$0")/../.." && pwd -P)
14
+ if [ -z "$root" ]; then
15
+ root="$own_root"
7
16
  else
8
- echo "No AGENTS.md file found. Can't symlink."
17
+ root=$(CDPATH= cd "$root" && pwd -P)
9
18
  fi
19
+ if [ "$root" != "$own_root" ]; then
20
+ echo "link-agentfiles: refusing to act on $root; this script belongs to $own_root" >&2
21
+ echo "link-agentfiles: a nested repo must ship its own .mise/scripts copy" >&2
22
+ exit 1
23
+ fi
24
+
25
+ CDPATH= cd "$root"
26
+ if [ ! -f AGENTS.md ]; then
27
+ echo "No AGENTS.md in $root. Nothing to symlink."
28
+ exit 0
29
+ fi
30
+
31
+ # `ln -sf` cannot tell a stale link it owns from a real file it must not touch:
32
+ # -f means "unlink whatever is there". A hand-written CLAUDE.md was silently
33
+ # replaced by a symlink, and the script printed a green checkmark while doing it.
34
+ for name in CLAUDE.md GEMINI.md; do
35
+ if [ -e "$name" ] && [ ! -L "$name" ]; then
36
+ echo "link-agentfiles: refusing to replace the real file $root/$name with a symlink" >&2
37
+ echo "link-agentfiles: move its content into AGENTS.md, then delete $name" >&2
38
+ exit 1
39
+ fi
40
+ ln -sfn AGENTS.md "$name"
41
+ done
42
+ echo "✅ AGENTS.md links verified in $root"
@@ -29,6 +29,7 @@ from __future__ import annotations
29
29
 
30
30
  import importlib.util
31
31
  import json
32
+ import argparse
32
33
  import os
33
34
  import re
34
35
  import shutil
@@ -286,13 +287,46 @@ def atomic_write(path: Path, content: bytes, mode: int) -> None:
286
287
  # --------------------------------------------------------------------------- #
287
288
 
288
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
+
289
322
  def provision(
290
323
  *,
324
+ root: str | None = None,
291
325
  after_preflight: Callable[[], None] | None = None,
292
326
  create_link: Callable[[Path, Path, int], None] | None = None,
293
327
  after_apply: Callable[[Path, Path], None] | None = None,
294
328
  ) -> int:
295
- project_root = Path.cwd().resolve(strict=True)
329
+ project_root = resolved_project_root(root)
296
330
  agents_path = project_root / ".agents"
297
331
  skills_path = agents_path / "skills"
298
332
  agents_existed = agents_path.exists() or agents_path.is_symlink()
@@ -387,10 +421,24 @@ def provision(
387
421
  )
388
422
  except OSError:
389
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()
390
437
  if (
391
438
  entry.name not in expected
392
439
  and entry.name not in managed_manifest_names
393
440
  and not link_targets_pack
441
+ and not dangling_link
394
442
  ):
395
443
  continue
396
444
  target = expected.get(entry.name)
@@ -514,8 +562,18 @@ def provision(
514
562
 
515
563
 
516
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()
517
575
  try:
518
- changed = provision()
576
+ changed = provision(root=args.root)
519
577
  except (FileNotFoundError, OSError, ValueError, RuntimeError, engine.PackUnavailable) as error:
520
578
  raise SystemExit(
521
579
  f"Skillex pack provisioning failed: {error}; "