@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.
- package/.mise/scripts/link-agentfiles.sh +38 -5
- package/README.md +20 -0
- package/dist/assets/project-notebook-skill/SHA256SUMS +10 -0
- package/dist/assets/project-notebook-skill/SKILL.md +64 -0
- package/dist/assets/project-notebook-skill/agents/openai.yaml +6 -0
- package/dist/assets/project-notebook-skill/export-manifest.json +56 -0
- package/dist/assets/project-notebook-skill/hooks/claude.settings.json +26 -0
- package/dist/assets/project-notebook-skill/hooks/hooks.master.json +26 -0
- package/dist/assets/project-notebook-skill/hooks/session-end.sh +228 -0
- package/dist/assets/project-notebook-skill/hooks/session-start.sh +228 -0
- package/dist/assets/project-notebook-skill/references/configuration.md +93 -0
- package/dist/assets/project-notebook-skill/references/recovery.md +54 -0
- package/dist/assets/project-notebook-skill/scripts/project-hooks.py +865 -0
- package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +848 -0
- package/dist/index.js +14320 -7847
- package/dist/mcp-server.js +13482 -8146
- package/package.json +4 -3
- package/templates/commonproject/copier.yml +14 -5
- package/templates/commonproject/template/.mise/scripts/link-agentfiles.sh +38 -5
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +60 -2
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +479 -24
- package/templates/commonproject/template/mise.toml.jinja +12 -6
- package/templates/hermes-agent/template/.gitignore.jinja +1 -0
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +11 -7
- package/templates/hermes-agent/template/.scripts/_lib.sh +54 -10
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +29 -2
- 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
|
|
|
@@ -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="$(
|
|
58
|
-
HB_DESCRIPTION="$(
|
|
59
|
-
TIMER_DESCRIPTION="$(
|
|
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="$(
|
|
65
|
-
RUNTIME_ENV_FILE="$(
|
|
66
|
-
GW_LOG_OUTPUT="$(
|
|
67
|
-
HB_LOG_OUTPUT="$(
|
|
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 (
|
|
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
|
|
52
|
+
import json, pathlib, re, sys
|
|
53
|
+
|
|
53
54
|
path, key, val = sys.argv[1:4]
|
|
54
|
-
p = pathlib.Path(path)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
sys.exit(f"yaml_set:
|
|
61
|
-
|
|
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-
|
|
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 (
|
|
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() {
|