@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
@@ -12,20 +12,26 @@ _.file = [".env"]
12
12
  # i.e. All linked agent files MUST be siblings at
13
13
  # any given level of nesting.
14
14
  #
15
+ # NOTE: every managed script is handed config_root EXPLICITLY. A mise enter
16
+ # hook runs with cwd set to the directory you cd'd into -- including a PARENT
17
+ # config's hook -- so a script that reads its subject from cwd reshapes
18
+ # whichever nested repo you entered. config_root locates the file; the
19
+ # argument locates the subject. The scripts refuse to run without it.
20
+ #
15
21
  # NOTE: hooks are an array of [[hooks.enter]] tables (NOT enter = [ ... ]) and
16
22
  # every {{config_root}} path is single-quoted so it survives a space in the
17
23
  # project path — mise runs each `script` via `sh -c`.
18
24
  [[hooks.enter]]
19
- script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
25
+ script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh' '{% raw %}{{config_root}}{% endraw %}'"
20
26
  [[hooks.enter]]
21
27
  # Atomic (PJAN-24/PJAN-57): shell complexity lives in the managed script so
22
28
  # mise never interpolates temporary variables. The script uses mktemp, traps
23
29
  # cleanup, and replaces .env only after op inject succeeds.
24
30
  script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/materialize-env.sh'"
25
31
  [[hooks.enter]]
26
- script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py'"
32
+ script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py' --root '{% raw %}{{config_root}}{% endraw %}'"
27
33
  [[hooks.enter]]
28
- script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project"
34
+ script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project --root '{% raw %}{{config_root}}{% endraw %}'"
29
35
  [[hooks.enter]]
30
36
  script = "[ -f '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' ] && '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' || true"
31
37
  {%- if agent_hooks_layer %}
@@ -51,16 +57,16 @@ task = "hooks:sync"
51
57
  {% endif %}
52
58
  [tasks."link:agentfiles"]
53
59
  description = "Symlink all agent files to AGENTS.md"
54
- run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
60
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh' '{% raw %}{{config_root}}{% endraw %}'"
55
61
 
56
62
  [tasks."skills:sync"]
57
63
  description = "Sync skills from manifest to local CLI dirs"
58
64
  depends = ["skills:provision:packs"]
59
- run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project"
65
+ run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project --root '{% raw %}{{config_root}}{% endraw %}'"
60
66
 
61
67
  [tasks."skills:provision:packs"]
62
68
  description = "Provision every Skillex pack declared in .agents/skills.json"
63
- run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py'"
69
+ run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py' --root '{% raw %}{{config_root}}{% endraw %}'"
64
70
  {% if agent_hooks_layer %}
65
71
  # --- Project-scoped agent hooks + skill fan-out (see .agents/hooks/README.md) ---
66
72
 
@@ -8,3 +8,4 @@ runtime/
8
8
  .scripts/.provision.log
9
9
  .scripts/.last-run
10
10
  .scripts/.done-*
11
+ .scripts/.plane-project-id
@@ -46,6 +46,10 @@ systemd_value() {
46
46
  "$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-value "$1" \
47
47
  || die "systemd value validation failed"
48
48
  }
49
+ systemd_scalar() {
50
+ "$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-scalar "$1" \
51
+ || die "systemd scalar validation failed"
52
+ }
49
53
  systemd_environment() {
50
54
  "$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-environment "$1" "$2" \
51
55
  || die "systemd environment validation failed"
@@ -54,17 +58,17 @@ systemd_exec_value() {
54
58
  "$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-exec-value "$1" \
55
59
  || die "systemd ExecStart value validation failed"
56
60
  }
57
- GW_DESCRIPTION="$(systemd_value "Hermes Gateway — $DISPLAY_NAME")"
58
- HB_DESCRIPTION="$(systemd_value "Hermes Heartbeat (reconcile + checkpoint) — $DISPLAY_NAME")"
59
- TIMER_DESCRIPTION="$(systemd_value "Heartbeat (reconcile + checkpoint) for $AGENT_ID")"
61
+ GW_DESCRIPTION="$(systemd_scalar "Hermes Gateway — $DISPLAY_NAME")"
62
+ HB_DESCRIPTION="$(systemd_scalar "Hermes Heartbeat (reconcile + checkpoint) — $DISPLAY_NAME")"
63
+ TIMER_DESCRIPTION="$(systemd_scalar "Heartbeat (reconcile + checkpoint) for $AGENT_ID")"
60
64
  ENV_HERMES_HOME="$(systemd_environment HERMES_HOME "$PROFILE_HOME")"
61
65
  ENV_HERMES_BIN="$(systemd_environment HERMES_BIN "$HERMES_BIN")"
62
66
  ENV_CODEX_HOME="$(systemd_environment CODEX_HOME "$CODEX_HOME")"
63
67
  ENV_TERMINAL_CWD="$(systemd_environment TERMINAL_CWD "$REPO_ROOT")"
64
- WORKING_DIRECTORY="$(systemd_value "$REPO_ROOT")"
65
- RUNTIME_ENV_FILE="$(systemd_value "-$RUNTIME/.env")"
66
- GW_LOG_OUTPUT="$(systemd_value "append:$RUNTIME/logs/gateway.systemd.log")"
67
- HB_LOG_OUTPUT="$(systemd_value "append:$RUNTIME/logs/heartbeat.log")"
68
+ WORKING_DIRECTORY="$(systemd_scalar "$REPO_ROOT")"
69
+ RUNTIME_ENV_FILE="$(systemd_scalar "-$RUNTIME/.env")"
70
+ GW_LOG_OUTPUT="$(systemd_scalar "append:$RUNTIME/logs/gateway.systemd.log")"
71
+ HB_LOG_OUTPUT="$(systemd_scalar "append:$RUNTIME/logs/heartbeat.log")"
68
72
  GW_EXEC_START="$(systemd_exec_value "$ROLE_DIR/.scripts/credential-launch.sh")"
69
73
  HB_EXEC_START="$GW_EXEC_START"
70
74
 
@@ -46,19 +46,63 @@ PYEOF
46
46
  # Apply a sed substitution to role.yaml in-place. Used to record IDs after
47
47
  # external provisioning steps return them.
48
48
  yaml_set() {
49
- # yaml_set KEY VALUE (only updates the first match; key must already exist)
49
+ # yaml_set KEY VALUE (supports flat and one-level nested keys)
50
50
  local key="$1" val="$2"
51
51
  python3 - "$ROLE_YAML" "$key" "$val" <<'PYEOF'
52
- import sys, re, pathlib
52
+ import json, pathlib, re, sys
53
+
53
54
  path, key, val = sys.argv[1:4]
54
- p = pathlib.Path(path); text = p.read_text()
55
- # Match `<indent><key>:<...>` and rewrite the value (last leaf only).
56
- leaf = key.split(".")[-1]
57
- new = re.sub(rf'(?m)^(\s*{re.escape(leaf)}:\s*)("?)[^"\n]*("?)\s*$',
58
- lambda m: f'{m.group(1)}"{val}"', text, count=1)
59
- if new == text:
60
- sys.exit(f"yaml_set: leaf '{leaf}' not found in {path}")
61
- p.write_text(new)
55
+ p = pathlib.Path(path)
56
+ text = p.read_text(encoding="utf-8")
57
+ parts = key.split(".")
58
+ if len(parts) not in (1, 2) or any(
59
+ not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_-]*", part) for part in parts
60
+ ):
61
+ sys.exit(f"yaml_set: unsupported key '{key}'")
62
+
63
+ lines = text.splitlines(keepends=True)
64
+ start, end, parent_indent = 0, len(lines), -1
65
+ if len(parts) == 2:
66
+ parent, leaf = parts
67
+ parents = []
68
+ for index, raw in enumerate(lines):
69
+ line = raw.rstrip("\r\n")
70
+ match = re.fullmatch(rf"( *){re.escape(parent)}:\s*(?:#.*)?", line)
71
+ if match and len(match.group(1)) == 0:
72
+ parents.append(index)
73
+ if len(parents) != 1:
74
+ sys.exit(f"yaml_set: parent '{parent}' not found uniquely in {path}")
75
+ parent_index = parents[0]
76
+ parent_indent = 0
77
+ start = parent_index + 1
78
+ for index in range(start, len(lines)):
79
+ candidate = lines[index].rstrip("\r\n")
80
+ if not candidate.strip() or candidate.lstrip().startswith("#"):
81
+ continue
82
+ indent = len(candidate) - len(candidate.lstrip(" "))
83
+ if indent <= parent_indent:
84
+ end = index
85
+ break
86
+ else:
87
+ leaf = parts[0]
88
+
89
+ targets = []
90
+ for index in range(start, end):
91
+ raw = lines[index]
92
+ line = raw.rstrip("\r\n")
93
+ match = re.match(rf"^( *){re.escape(leaf)}:\s*", line)
94
+ if not match:
95
+ continue
96
+ indent = len(match.group(1))
97
+ if (parent_indent < 0 and indent == 0) or (parent_indent >= 0 and indent > parent_indent):
98
+ targets.append((index, match.group(1)))
99
+ if len(targets) != 1:
100
+ sys.exit(f"yaml_set: key '{key}' not found uniquely in {path}")
101
+
102
+ index, indent = targets[0]
103
+ ending = "\r\n" if lines[index].endswith("\r\n") else "\n" if lines[index].endswith("\n") else ""
104
+ lines[index] = f"{indent}{leaf}: {json.dumps(val, ensure_ascii=False)}{ending}"
105
+ p.write_text("".join(lines), encoding="utf-8")
62
106
  PYEOF
63
107
  }
64
108
 
@@ -389,6 +389,28 @@ def serialize_systemd_value(value: str) -> str:
389
389
  return '"' + "".join(escaped) + '"'
390
390
 
391
391
 
392
+ def serialize_systemd_scalar(value: str) -> str:
393
+ """Return a scalar directive value without introducing literal quotes."""
394
+ validate_unicode(value)
395
+ if "\0" in value:
396
+ raise FleetEnvParseError(1, "NUL is not allowed in a systemd value")
397
+ if "\r" in value or "\n" in value:
398
+ raise FleetEnvParseError(1, "newline control characters are not allowed in a systemd value")
399
+ escaped: list[str] = []
400
+ for char in value:
401
+ if char == "\\":
402
+ escaped.append(r"\\")
403
+ elif char == "%":
404
+ escaped.append("%%")
405
+ elif char == "\t":
406
+ escaped.append(r"\t")
407
+ elif ord(char) < 0x20 or ord(char) == 0x7F:
408
+ escaped.append(f"\\x{ord(char):02x}")
409
+ else:
410
+ escaped.append(char)
411
+ return "".join(escaped)
412
+
413
+
392
414
  def serialize_systemd_environment(name: str, value: str) -> str:
393
415
  if not NAME.fullmatch(name):
394
416
  raise FleetEnvParseError(1, "invalid systemd environment variable name")
@@ -685,12 +707,14 @@ def main() -> int:
685
707
  parse_mode = len(sys.argv) == 2
686
708
  upsert_mode = len(sys.argv) == 5 and sys.argv[1] == "--upsert"
687
709
  systemd_value_mode = len(sys.argv) == 3 and sys.argv[1] == "--systemd-value"
710
+ systemd_scalar_mode = len(sys.argv) == 3 and sys.argv[1] == "--systemd-scalar"
688
711
  systemd_exec_value_mode = len(sys.argv) == 3 and sys.argv[1] == "--systemd-exec-value"
689
712
  systemd_environment_mode = len(sys.argv) == 4 and sys.argv[1] == "--systemd-environment"
690
- if not parse_mode and not upsert_mode and not systemd_value_mode and not systemd_exec_value_mode and not systemd_environment_mode:
713
+ if not parse_mode and not upsert_mode and not systemd_value_mode and not systemd_scalar_mode and not systemd_exec_value_mode and not systemd_environment_mode:
691
714
  print(
692
715
  "usage: parse-fleet-env.py PATH | --upsert PATH KEY VALUE | "
693
- "--systemd-value VALUE | --systemd-exec-value VALUE | --systemd-environment NAME VALUE",
716
+ "--systemd-value VALUE | --systemd-scalar VALUE | "
717
+ "--systemd-exec-value VALUE | --systemd-environment NAME VALUE",
694
718
  file=sys.stderr,
695
719
  )
696
720
  return 2
@@ -698,6 +722,9 @@ def main() -> int:
698
722
  if systemd_value_mode:
699
723
  print(serialize_systemd_value(sys.argv[2]), end="")
700
724
  return 0
725
+ if systemd_scalar_mode:
726
+ print(serialize_systemd_scalar(sys.argv[2]), end="")
727
+ return 0
701
728
  if systemd_exec_value_mode:
702
729
  print(serialize_systemd_exec_value(sys.argv[2]), end="")
703
730
  return 0
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env sh
2
2
  # Plane ticket-provider adapter.
3
3
  #
4
- # Credentials: PLANE_API_KEY or PLANE_<WORKSPACE>_API_KEY (X-API-Key header)
4
+ # Credentials: PLANE_API_KEY or PLANE_<WORKSPACE>_API_KEY (raw at runtime or
5
+ # an op:// reference resolved immediately before use)
5
6
  # Endpoint: PLANE_BASE (default https://plane.delo.sh)
6
7
  # Board binding (repo-root .project.json `ticket_provider:`):
7
8
  # workspace: <workspace-slug> (or env PLANE_WORKSPACE)
@@ -59,6 +60,20 @@ print(value, end="")
59
60
  PY
60
61
  }
61
62
 
63
+ # Resolve an approved secret reference only after selecting the exact provider
64
+ # key. The shared dotenv stays inert and the resolved value exists only in this
65
+ # provider process.
66
+ resolve_secret_value() {
67
+ value="${1:-}"
68
+ case "$value" in
69
+ op://*)
70
+ command -v op >/dev/null 2>&1 || die "1Password CLI is required for the configured Plane credential"
71
+ op read "$value" || die "failed to resolve the configured Plane credential"
72
+ ;;
73
+ *) printf '%s' "$value" ;;
74
+ esac
75
+ }
76
+
62
77
  tp_cfg() {
63
78
  [ -f "$ROLE_YAML" ] || return 0
64
79
  python3 - "$ROLE_YAML" "$1" <<'PY'
@@ -103,8 +118,9 @@ if [ -z "${PLANE_API_KEY:-}" ]; then
103
118
  if [ -z "${PLANE_API_KEY:-}" ] && [ -f "$FLEET_ENV" ]; then
104
119
  PLANE_API_KEY="$(dotenv_value "$FLEET_ENV" "$KEY")"
105
120
  fi
106
- export PLANE_API_KEY
107
121
  fi
122
+ PLANE_API_KEY="$(resolve_secret_value "${PLANE_API_KEY:-}")"
123
+ export PLANE_API_KEY
108
124
 
109
125
  # api METHOD PATH [JSON_BODY] — call Plane REST, print response body.
110
126
  api() {