@delorenj/pjangler 1.2.9 → 1.2.13
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/dist/index.js +498 -204
- package/dist/mcp-server.js +488 -194
- package/package.json +1 -1
- package/templates/commonproject/copier.yml +4 -1
- package/templates/commonproject/template/.project.json.jinja +9 -12
- package/templates/commonproject/template/mise.toml.jinja +1 -1
- package/templates/hermes-agent/README.md +1 -1
- package/templates/hermes-agent/config.example.toml +66 -0
- package/templates/hermes-agent/docs/architecture.md +5 -0
- package/templates/hermes-agent/docs/bloodbank-gateway.md +57 -0
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +60 -0
- package/templates/hermes-agent/docs/operations.md +1 -1
- package/templates/hermes-agent/docs/sentinel/README.md +2 -2
- package/templates/hermes-agent/docs/sentinel/development.md +3 -3
- package/templates/hermes-agent/docs/sentinel/providers.md +6 -5
- package/templates/hermes-agent/install-local.sh +10 -4
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +5 -3
- package/templates/hermes-agent/scripts/fleet-sync.sh +64 -1
- package/templates/hermes-agent/template/.gitignore.jinja +2 -0
- package/templates/hermes-agent/template/.scripts/01-config.sh +1 -0
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +9 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +22 -3
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +22 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -33
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +8 -1
- package/templates/hermes-agent/template/.scripts/_lib.sh +61 -3
- package/templates/hermes-agent/template/.scripts/config.example.toml +5 -0
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +66 -22
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +3 -3
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +29 -9
- package/templates/hermes-agent/template/role.yaml.jinja +2 -27
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
# 2. Otherwise (hermes run on a repo with no CommonProject board yet)
|
|
11
11
|
# -> create ONE repo-named board and write it back into .project.json so
|
|
12
12
|
# it becomes the SOT for every agent in this repo.
|
|
13
|
-
# Either way we register this agent under .project.json `agents
|
|
14
|
-
#
|
|
13
|
+
# Either way we register this agent under .project.json `agents`. role.yaml is
|
|
14
|
+
# identity/personality only; it must not carry board UUIDs or derived URLs.
|
|
15
15
|
# shellcheck source=_lib.sh
|
|
16
16
|
source "$(dirname "$0")/_lib.sh"
|
|
17
17
|
load_role_env
|
|
18
|
+
resolve_plane_api_key "$PLANE_WORKSPACE"
|
|
18
19
|
# shellcheck source=lib/ticket-provider.sh
|
|
19
20
|
source "$(dirname "$0")/lib/ticket-provider.sh"
|
|
20
21
|
|
|
@@ -46,13 +47,13 @@ PY
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
# pj_write — merge board binding (optional) + this agent into .project.json.
|
|
49
|
-
# args: set_provider(0|1) provider board_id
|
|
50
|
+
# args: set_provider(0|1) provider board_id workspace identifier
|
|
50
51
|
pj_write() {
|
|
51
52
|
REPO="$REPO" REPO_ROOT="$REPO_ROOT" AGENT_ID="$AGENT_ID" ROLE="$ROLE" \
|
|
52
53
|
ROLE_DIR_REL="$ROLE_DIR_REL" PROJECT_DESC="${PROJECT_DESC:-}" \
|
|
53
54
|
python3 - "$PROJECT_JSON" "$@" <<'PY'
|
|
54
55
|
import sys, os, json, pathlib
|
|
55
|
-
(path, set_provider, provider, board_id,
|
|
56
|
+
(path, set_provider, provider, board_id, workspace, identifier) = sys.argv[1:7]
|
|
56
57
|
p = pathlib.Path(path)
|
|
57
58
|
try:
|
|
58
59
|
d = json.loads(p.read_text())
|
|
@@ -71,43 +72,28 @@ if set_provider == "1":
|
|
|
71
72
|
if workspace: tp["workspace"] = workspace
|
|
72
73
|
if identifier: tp["identifier"] = identifier
|
|
73
74
|
if board_id: tp["board_id"] = board_id
|
|
74
|
-
|
|
75
|
+
tp.pop("board_url", None)
|
|
76
|
+
tp["state"] = "linked" if board_id else tp.get("state", "planned")
|
|
75
77
|
ag = d.setdefault("agents", {})
|
|
76
78
|
ag[os.environ["AGENT_ID"]] = {"role": os.environ["ROLE"], "role_dir": os.environ["ROLE_DIR_REL"]}
|
|
79
|
+
automation = d.setdefault("automation", {})
|
|
80
|
+
automation.setdefault("reconcile", {"enabled": False, "grace_hours": 0, "auto_review": True})
|
|
77
81
|
p.write_text(json.dumps(d, indent=2) + "\n")
|
|
78
82
|
PY
|
|
79
83
|
log " .project.json updated (agent=$AGENT_ID)"
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
local provider="$1"
|
|
86
|
+
# Keep only the legacy adapter name in role.yaml. Stable board identity belongs
|
|
87
|
+
# in .project.json.
|
|
88
|
+
mirror_provider_name_to_role_yaml() {
|
|
89
|
+
local provider="$1"
|
|
86
90
|
yaml_set ticket_provider.name "$provider" 2>/dev/null || true
|
|
87
|
-
[ -n "$bid" ] && yaml_set ticket_provider.board_id "$bid" 2>/dev/null || true
|
|
88
|
-
[ -n "$burl" ] && yaml_set ticket_provider.board_url "$burl" 2>/dev/null || true
|
|
89
|
-
case "$provider" in
|
|
90
|
-
plane)
|
|
91
|
-
[ -n "$bid" ] && echo "$bid" > "$ROLE_DIR/.scripts/.plane-project-id"
|
|
92
|
-
[ -n "$ws" ] && yaml_set ticket_provider.workspace "$ws" 2>/dev/null || true
|
|
93
|
-
[ -n "$bid" ] && yaml_set ticket_provider.project "$bid" 2>/dev/null || true
|
|
94
|
-
[ -n "$ws" ] && yaml_set plane.workspace "$ws" 2>/dev/null || true
|
|
95
|
-
[ -n "$ident" ] && yaml_set plane.identifier "$ident" 2>/dev/null || true
|
|
96
|
-
;;
|
|
97
|
-
trello)
|
|
98
|
-
[ -n "$bid" ] && yaml_set ticket_provider.board "$bid" 2>/dev/null || true
|
|
99
|
-
;;
|
|
100
|
-
*)
|
|
101
|
-
die "unsupported ticket provider: $provider (expected plane|trello)"
|
|
102
|
-
;;
|
|
103
|
-
esac
|
|
104
91
|
}
|
|
105
92
|
|
|
106
93
|
# ── Provider resolution ──────────────────────────────────────────────────
|
|
107
94
|
# An existing repo board (in .project.json) wins — every agent binds to it.
|
|
108
95
|
SOT_TYPE="$(pj ticket_provider.type)"
|
|
109
96
|
SOT_BOARD_ID="$(pj ticket_provider.board_id)"
|
|
110
|
-
SOT_URL="$(pj ticket_provider.board_url)"
|
|
111
97
|
SOT_WS="$(pj ticket_provider.workspace)"
|
|
112
98
|
SOT_IDENT="$(pj ticket_provider.identifier)"
|
|
113
99
|
|
|
@@ -122,8 +108,8 @@ if [ -n "$SOT_BOARD_ID" ]; then
|
|
|
122
108
|
fi
|
|
123
109
|
case "$PROVIDER" in plane|trello) ;; *) die "unsupported ticket provider in .project.json: $PROVIDER (expected plane|trello)" ;; esac
|
|
124
110
|
log "[42] binding $AGENT_ID to existing repo board (provider=$PROVIDER, id=$SOT_BOARD_ID)"
|
|
125
|
-
|
|
126
|
-
pj_write
|
|
111
|
+
mirror_provider_name_to_role_yaml "$PROVIDER"
|
|
112
|
+
pj_write 1 "$PROVIDER" "$SOT_BOARD_ID" "$SOT_WS" "$SOT_IDENT"
|
|
127
113
|
mark_done 42-ticket-provider
|
|
128
114
|
exit 0
|
|
129
115
|
fi
|
|
@@ -146,16 +132,15 @@ case "$PROVIDER" in
|
|
|
146
132
|
KEYVAR=PLANE_API_KEY; [ "$PROVIDER" = trello ] && KEYVAR=TRELLO_KEY
|
|
147
133
|
if [[ -z "${!KEYVAR:-}" ]]; then
|
|
148
134
|
warn "[42] $KEYVAR not set; skipping board creation. Set creds and re-run ./.scripts/42-ticket-provider.sh"
|
|
149
|
-
pj_write 1 "$PROVIDER" "" "
|
|
135
|
+
pj_write 1 "$PROVIDER" "" "${SOT_WS:-${PLANE_WORKSPACE:-}}" "$IDENT"
|
|
150
136
|
mark_done 42-ticket-provider; exit 0
|
|
151
137
|
fi
|
|
152
138
|
OUT="$(tp create_board "$NAME" "$IDENT" "$DESC")" || die "create_board failed for $PROVIDER"
|
|
153
139
|
BID="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("board_id",""))')"
|
|
154
|
-
BURL="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("board_url",""))')"
|
|
155
140
|
WS="${SOT_WS:-${PLANE_WORKSPACE:-}}"
|
|
156
141
|
[ "$PROVIDER" = trello ] && WS=""
|
|
157
|
-
|
|
158
|
-
pj_write 1 "$PROVIDER" "$BID" "$
|
|
142
|
+
mirror_provider_name_to_role_yaml "$PROVIDER"
|
|
143
|
+
pj_write 1 "$PROVIDER" "$BID" "$WS" "$IDENT"
|
|
159
144
|
;;
|
|
160
145
|
|
|
161
146
|
*) die "unknown ticket provider: $PROVIDER (expected plane|trello)" ;;
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
source "$(dirname "$0")/_lib.sh"
|
|
6
6
|
load_role_env
|
|
7
7
|
|
|
8
|
-
already_done 70-systemd &&
|
|
8
|
+
if already_done 70-systemd && [[ "${FORCE_SYSTEMD:-0}" != "1" ]]; then
|
|
9
|
+
log "[70] systemd already installed — skipping"
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
9
12
|
[[ "${SKIP_SYSTEMD:-0}" == "1" ]] && { log "[70] systemd — SKIPPED"; mark_done 70-systemd; exit 0; }
|
|
10
13
|
|
|
11
14
|
RUNTIME="$ROLE_DIR/runtime"
|
|
@@ -34,6 +37,7 @@ Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
|
34
37
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
35
38
|
Environment=HERMES_KANBAN_DISPATCH_IN_GATEWAY=0
|
|
36
39
|
Environment=HERMES_KANBAN_SLASH_ENABLED=0
|
|
40
|
+
EnvironmentFile=-$FLEET_ENV
|
|
37
41
|
EnvironmentFile=-$RUNTIME/.env
|
|
38
42
|
ExecStart=$HERMES_BIN gateway run --replace
|
|
39
43
|
Restart=on-failure
|
|
@@ -59,6 +63,7 @@ WorkingDirectory=$RUNTIME
|
|
|
59
63
|
Environment=HERMES_HOME=$RUNTIME
|
|
60
64
|
Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
61
65
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
66
|
+
EnvironmentFile=-$FLEET_ENV
|
|
62
67
|
EnvironmentFile=-$RUNTIME/.env
|
|
63
68
|
ExecStart=$HERMES_AGENT_REPO/.venv/bin/python $RUNTIME/bloodbank-consumer.py
|
|
64
69
|
Restart=on-failure
|
|
@@ -88,6 +93,7 @@ WorkingDirectory=$REPO_ROOT
|
|
|
88
93
|
Environment=HERMES_HOME=$RUNTIME
|
|
89
94
|
Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
90
95
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
96
|
+
EnvironmentFile=-$FLEET_ENV
|
|
91
97
|
EnvironmentFile=-%h/.config/hermes-agent/env
|
|
92
98
|
EnvironmentFile=-%h/.hermes/env
|
|
93
99
|
EnvironmentFile=-%h/.hermes/hermes-agent.env
|
|
@@ -114,6 +120,7 @@ UNIT
|
|
|
114
120
|
|
|
115
121
|
if systemd_user_available; then
|
|
116
122
|
systemctl --user daemon-reload
|
|
123
|
+
systemctl --user disable --now "hermes-${AGENT_ID}-checkpoint.timer" "hermes-${AGENT_ID}-checkpoint.service" >/dev/null 2>&1 || true
|
|
117
124
|
# `enable --now` both enables (persist across login) AND starts the unit now, so a
|
|
118
125
|
# freshly provisioned agent comes up live instead of dormant. Units with missing
|
|
119
126
|
# creds (e.g. a gateway with no Telegram token yet) fail softly via Restart=on-failure.
|
|
@@ -62,6 +62,41 @@ p.write_text(new)
|
|
|
62
62
|
PYEOF
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
# Locate the repo-root .project.json for this role, if present.
|
|
66
|
+
project_json_path() {
|
|
67
|
+
local root
|
|
68
|
+
root="$(project_repo_path 2>/dev/null || true)"
|
|
69
|
+
[[ -n "$root" && -f "$root/.project.json" ]] && { printf '%s' "$root/.project.json"; return 0; }
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# project_json_get <dotted.key> — read a string/scalar from repo-root
|
|
74
|
+
# .project.json. This is the deterministic SOT for project wiring.
|
|
75
|
+
project_json_get() {
|
|
76
|
+
local key="$1" path
|
|
77
|
+
path="$(project_json_path 2>/dev/null || true)"
|
|
78
|
+
[[ -f "$path" ]] || { printf ''; return 0; }
|
|
79
|
+
python3 - "$path" "$key" <<'PYEOF'
|
|
80
|
+
import json
|
|
81
|
+
import pathlib
|
|
82
|
+
import sys
|
|
83
|
+
|
|
84
|
+
path, key = sys.argv[1:3]
|
|
85
|
+
try:
|
|
86
|
+
cur = json.loads(pathlib.Path(path).read_text())
|
|
87
|
+
except Exception:
|
|
88
|
+
print("", end="")
|
|
89
|
+
raise SystemExit(0)
|
|
90
|
+
for part in key.split("."):
|
|
91
|
+
if isinstance(cur, dict) and part in cur:
|
|
92
|
+
cur = cur[part]
|
|
93
|
+
else:
|
|
94
|
+
print("", end="")
|
|
95
|
+
raise SystemExit(0)
|
|
96
|
+
print(cur if isinstance(cur, (str, int, float, bool)) else "", end="")
|
|
97
|
+
PYEOF
|
|
98
|
+
}
|
|
99
|
+
|
|
65
100
|
# ─── Distributable config (~/.config/hermes-agent-template/config.toml) ──────
|
|
66
101
|
# Single source of truth for environment-specific defaults so this template can
|
|
67
102
|
# be handed to someone else without editing any script. Ship config.example.toml
|
|
@@ -112,8 +147,10 @@ load_role_env() {
|
|
|
112
147
|
BOT_HANDLE=$(yaml_get telegram.bot_username)
|
|
113
148
|
PROFILE_NAME=$(yaml_get profile)
|
|
114
149
|
|
|
115
|
-
# Plane workspace:
|
|
116
|
-
PLANE_WORKSPACE=$(
|
|
150
|
+
# Plane workspace: repo-root .project.json wins; role.yaml is legacy fallback.
|
|
151
|
+
PLANE_WORKSPACE=$(project_json_get ticket_provider.workspace)
|
|
152
|
+
[[ -n "$PLANE_WORKSPACE" ]] || PLANE_WORKSPACE=$(yaml_get ticket_provider.workspace)
|
|
153
|
+
[[ -n "$PLANE_WORKSPACE" ]] || PLANE_WORKSPACE=$(yaml_get plane.workspace)
|
|
117
154
|
[[ -n "$PLANE_WORKSPACE" ]] || PLANE_WORKSPACE=$(config_get plane.workspace "33god")
|
|
118
155
|
|
|
119
156
|
# Runtime repo: role.yaml stores the bare repo name plus an optional owner.
|
|
@@ -129,6 +166,27 @@ load_role_env() {
|
|
|
129
166
|
PLANE_WORKSPACE RUNTIME_REPO PROFILE_NAME
|
|
130
167
|
}
|
|
131
168
|
|
|
169
|
+
plane_workspace_key() {
|
|
170
|
+
local workspace="${1:-${PLANE_WORKSPACE:-}}"
|
|
171
|
+
workspace="$(printf '%s' "$workspace" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')"
|
|
172
|
+
[[ -n "$workspace" ]] || workspace="DEFAULT"
|
|
173
|
+
printf 'PLANE_%s_API_KEY' "$workspace"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
resolve_plane_api_key() {
|
|
177
|
+
if [[ -n "${PLANE_API_KEY:-}" ]]; then
|
|
178
|
+
export PLANE_API_KEY
|
|
179
|
+
return 0
|
|
180
|
+
fi
|
|
181
|
+
local key value
|
|
182
|
+
key="$(plane_workspace_key "${1:-${PLANE_WORKSPACE:-}}")"
|
|
183
|
+
value="${!key:-}"
|
|
184
|
+
if [[ -n "$value" ]]; then
|
|
185
|
+
PLANE_API_KEY="$value"
|
|
186
|
+
export PLANE_API_KEY
|
|
187
|
+
fi
|
|
188
|
+
}
|
|
189
|
+
|
|
132
190
|
# Skip a step if previously completed (idempotent reruns).
|
|
133
191
|
already_done() {
|
|
134
192
|
local marker="$ROLE_DIR/.scripts/.done-$1"
|
|
@@ -165,7 +223,7 @@ BLOODBANK_COMPOSE_DIR="${BLOODBANK_COMPOSE_DIR:-$(config_get bloodbank.compose_d
|
|
|
165
223
|
|
|
166
224
|
# Plane
|
|
167
225
|
PLANE_BASE="${PLANE_BASE:-$(config_get plane.base 'https://plane.delo.sh')}"
|
|
168
|
-
PLANE_API_KEY="${PLANE_API_KEY
|
|
226
|
+
PLANE_API_KEY="${PLANE_API_KEY:-}"
|
|
169
227
|
|
|
170
228
|
export FLEET_ENV HERMES_BIN HERMES_AGENT_REPO HERMES_OAUTH_FILE CODEX_HOME \
|
|
171
229
|
RUNTIME_SCAFFOLD_DIR REGISTRY_FILE \
|
|
@@ -27,6 +27,11 @@ oauth_file = "~/.hermes/auth.json"
|
|
|
27
27
|
codex_home = "~/.codex"
|
|
28
28
|
# Canonical external skills dir mirrored into each agent profile.
|
|
29
29
|
canonical_skills_dir = "/home/delorenj/.agents/skills"
|
|
30
|
+
# PM-only external skill libraries surfaced alongside Hermes built-ins.
|
|
31
|
+
pm_external_skill_dirs = [
|
|
32
|
+
"~/code/skillex/skill-sets/global/.system",
|
|
33
|
+
"~/code/skillex/packs/bmad/6.10.2",
|
|
34
|
+
]
|
|
30
35
|
# Voxxy plugin + service defaults for Hermes PM agents.
|
|
31
36
|
voxxy_plugin_dir = "~/code/voxxy/plugins/tts/voxxy"
|
|
32
37
|
vox_url = "https://vox.delo.sh"
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# reconciliation pass when local state says no worker is active, the worker
|
|
7
7
|
# heartbeat is stale, or the last full run is outside the cooldown window. The
|
|
8
8
|
# full pass executes the role's sentinel.prompt.md, which reasons about tickets
|
|
9
|
-
# through the ticket-provider adapter (
|
|
9
|
+
# through the ticket-provider adapter (Plane | Trello) — never a
|
|
10
10
|
# hardcoded backend. After the sentinel decision (skip OR full), it
|
|
11
11
|
# opportunistically checkpoints the runtime submodule (commit+push) at most once
|
|
12
12
|
# per HEARTBEAT_CHECKPOINT_MIN_INTERVAL_SECONDS, so memory/session state stays
|
|
@@ -19,12 +19,18 @@ PROMPT_FILE="$ROLE_DIR/.scripts/sentinel.prompt.md"
|
|
|
19
19
|
STATE_FILE="$RUNTIME/continuous-ticket-sentinel-state.json"
|
|
20
20
|
LOCK_FILE="$RUNTIME/continuous-ticket-sentinel.lock"
|
|
21
21
|
ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
22
|
+
FLEET_ENV="${HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
|
|
22
23
|
LOG_FILE="$RUNTIME/logs/heartbeat.log"
|
|
23
24
|
CHECKPOINT_BIN="$ROLE_DIR/.scripts/checkpoint.sh"
|
|
24
25
|
CHECKPOINT_STAMP="$RUNTIME/.last-checkpoint"
|
|
25
26
|
|
|
27
|
+
if [[ -f "$FLEET_ENV" ]]; then
|
|
28
|
+
# shellcheck disable=SC1090
|
|
29
|
+
source "$FLEET_ENV"
|
|
30
|
+
fi
|
|
31
|
+
|
|
26
32
|
# Hermes binary: explicit env > ~/.config/hermes-agent/hermes-bin > PATH.
|
|
27
|
-
HERMES_BIN="${HERMES_BIN:-}"
|
|
33
|
+
HERMES_BIN="${HERMES_BIN:-${HERMES_FLEET_BIN:-}}"
|
|
28
34
|
if [[ -z "$HERMES_BIN" ]]; then
|
|
29
35
|
if [[ -r "$HOME/.config/hermes-agent/hermes-bin" ]]; then
|
|
30
36
|
HERMES_BIN="$(cat "$HOME/.config/hermes-agent/hermes-bin")"
|
|
@@ -80,13 +86,61 @@ print(value.strip().strip('"').strip("'"))
|
|
|
80
86
|
PYEOF
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
repo_root() {
|
|
90
|
+
local dir="$ROLE_DIR"
|
|
91
|
+
for _ in 1 2 3 4 5; do
|
|
92
|
+
dir="$(dirname "$dir")"
|
|
93
|
+
if [[ -d "$dir/.git" || -f "$dir/.git" ]]; then printf '%s\n' "$dir"; return 0; fi
|
|
94
|
+
done
|
|
95
|
+
return 1
|
|
96
|
+
}
|
|
97
|
+
REPO_ROOT="$(repo_root)"
|
|
98
|
+
PROJECT_JSON="$REPO_ROOT/.project.json"
|
|
99
|
+
|
|
100
|
+
project_json_value() {
|
|
101
|
+
python3 - "$PROJECT_JSON" "$1" <<'PYEOF'
|
|
102
|
+
import json
|
|
103
|
+
import pathlib
|
|
104
|
+
import sys
|
|
105
|
+
|
|
106
|
+
path, key = sys.argv[1:3]
|
|
107
|
+
try:
|
|
108
|
+
cur = json.loads(pathlib.Path(path).read_text())
|
|
109
|
+
except Exception:
|
|
110
|
+
print("")
|
|
111
|
+
raise SystemExit(0)
|
|
112
|
+
for part in key.split("."):
|
|
113
|
+
if isinstance(cur, dict) and part in cur:
|
|
114
|
+
cur = cur[part]
|
|
115
|
+
else:
|
|
116
|
+
print("")
|
|
117
|
+
raise SystemExit(0)
|
|
118
|
+
print(cur if isinstance(cur, (str, int, float, bool)) else "")
|
|
119
|
+
PYEOF
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Project-owned automation gate. Legacy role.yaml reconcile.enabled is honored
|
|
123
|
+
# only for older agents that have not migrated their .project.json yet.
|
|
85
124
|
reconcile_enabled() {
|
|
86
|
-
python3 - "$ROLE_YAML" <<'PYEOF'
|
|
87
|
-
import
|
|
125
|
+
python3 - "$PROJECT_JSON" "$ROLE_YAML" <<'PYEOF'
|
|
126
|
+
import json
|
|
127
|
+
import re
|
|
128
|
+
import sys
|
|
88
129
|
from pathlib import Path
|
|
89
|
-
|
|
130
|
+
project_path, role_path = map(Path, sys.argv[1:3])
|
|
131
|
+
try:
|
|
132
|
+
project = json.loads(project_path.read_text())
|
|
133
|
+
except Exception:
|
|
134
|
+
project = {}
|
|
135
|
+
reconcile = ((project.get("automation") or {}).get("reconcile") or {}) if isinstance(project, dict) else {}
|
|
136
|
+
if "enabled" in reconcile:
|
|
137
|
+
value = reconcile.get("enabled")
|
|
138
|
+
print("true" if str(value).lower() == "true" else "false")
|
|
139
|
+
raise SystemExit(0)
|
|
140
|
+
try:
|
|
141
|
+
text = role_path.read_text()
|
|
142
|
+
except Exception:
|
|
143
|
+
text = ""
|
|
90
144
|
m = re.search(r'(?m)^reconcile:[ \t]*\n((?:[ \t]+\S.*\n?)*)', text)
|
|
91
145
|
block = m.group(1) if m else ""
|
|
92
146
|
me = re.search(r'(?m)^[ \t]+enabled:[ \t]*"?([A-Za-z]+)"?', block)
|
|
@@ -96,17 +150,8 @@ PYEOF
|
|
|
96
150
|
|
|
97
151
|
AGENT_ID="$(yaml_value agent_id)"
|
|
98
152
|
REPO_NAME="$(yaml_value repo)"
|
|
99
|
-
PROVIDER="$(
|
|
100
|
-
|
|
101
|
-
repo_root() {
|
|
102
|
-
local dir="$ROLE_DIR"
|
|
103
|
-
for _ in 1 2 3 4 5; do
|
|
104
|
-
dir="$(dirname "$dir")"
|
|
105
|
-
if [[ -d "$dir/.git" || -f "$dir/.git" ]]; then printf '%s\n' "$dir"; return 0; fi
|
|
106
|
-
done
|
|
107
|
-
return 1
|
|
108
|
-
}
|
|
109
|
-
REPO_ROOT="$(repo_root)"
|
|
153
|
+
PROVIDER="$(project_json_value ticket_provider.type)"
|
|
154
|
+
[[ -n "$PROVIDER" ]] || PROVIDER="$(yaml_block_value ticket_provider name)"
|
|
110
155
|
cd "$REPO_ROOT"
|
|
111
156
|
mkdir -p "$RUNTIME/logs"
|
|
112
157
|
|
|
@@ -132,11 +177,10 @@ else
|
|
|
132
177
|
fi
|
|
133
178
|
|
|
134
179
|
# Reconcile gate: the autonomous board-reconciliation pass runs only when
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
# reconcile.enabled to opt a repo into autonomous board reconciliation.
|
|
180
|
+
# repo-root .project.json automation.reconcile.enabled is true. Default off →
|
|
181
|
+
# the heartbeat just checkpoints (legacy checkpoint-timer behavior).
|
|
138
182
|
if [[ "$(reconcile_enabled)" != "true" ]]; then
|
|
139
|
-
printf '[heartbeat] reconcile disabled (reconcile.enabled != true) — checkpoint-only tick\n'
|
|
183
|
+
printf '[heartbeat] reconcile disabled (automation.reconcile.enabled != true) — checkpoint-only tick\n'
|
|
140
184
|
maybe_checkpoint
|
|
141
185
|
exit 0
|
|
142
186
|
fi
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
# sentinel engine and a concrete ticket system (Plane | Trello).
|
|
4
4
|
#
|
|
5
5
|
# The engine NEVER calls a provider directly. It calls `tp <op> [args...]`,
|
|
6
|
-
# which dispatches to providers/<provider>.sh.
|
|
7
|
-
#
|
|
6
|
+
# which dispatches to providers/<provider>.sh. Repo-root .project.json owns the
|
|
7
|
+
# provider/board binding; role.yaml is only a legacy provider-name fallback.
|
|
8
8
|
#
|
|
9
9
|
# Contract (operations every provider must implement):
|
|
10
10
|
# resolve -> JSON {provider, board_id, board_url}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# create_board <name> <id> <d> -> JSON {board_id, board_url}
|
|
20
20
|
#
|
|
21
21
|
# Each provider reads its credentials from the environment (see providers/*.sh
|
|
22
|
-
# headers) and the board binding from
|
|
22
|
+
# headers) and the board binding from repo-root .project.json.
|
|
23
23
|
|
|
24
24
|
# Resolve the provider name: explicit env wins, then repo-root .project.json
|
|
25
25
|
# (the SOT), then role.yaml (self-parsed so this works even when _lib.sh /
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
2
|
# Plane ticket-provider adapter.
|
|
3
3
|
#
|
|
4
|
-
# Credentials: PLANE_API_KEY
|
|
4
|
+
# Credentials: PLANE_API_KEY or PLANE_<WORKSPACE>_API_KEY (X-API-Key header)
|
|
5
5
|
# Endpoint: PLANE_BASE (default https://plane.delo.sh)
|
|
6
|
-
# Board binding (
|
|
7
|
-
# name: plane
|
|
6
|
+
# Board binding (repo-root .project.json `ticket_provider:`):
|
|
8
7
|
# workspace: <workspace-slug> (or env PLANE_WORKSPACE)
|
|
9
|
-
#
|
|
8
|
+
# board_id: <project-uuid> (set by create_board / 42-ticket-provider)
|
|
10
9
|
# state_map: { in_review: "In Review", completed: "Done" } optional
|
|
11
10
|
#
|
|
12
11
|
# Plane model: project = board, cycle = milestone, state.group in
|
|
@@ -21,9 +20,21 @@ ROLE_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
|
21
20
|
ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
22
21
|
BASE="${PLANE_BASE:-https://plane.delo.sh}"
|
|
23
22
|
|
|
23
|
+
FLEET_ENV="${HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
|
|
24
|
+
if [ -f "$FLEET_ENV" ]; then
|
|
25
|
+
# shellcheck disable=SC1090
|
|
26
|
+
. "$FLEET_ENV"
|
|
27
|
+
fi
|
|
28
|
+
|
|
24
29
|
die() { echo "plane: $*" >&2; exit 1; }
|
|
25
30
|
need_key() { [ -n "${PLANE_API_KEY:-}" ] || die "PLANE_API_KEY is not set"; }
|
|
26
31
|
|
|
32
|
+
workspace_key() {
|
|
33
|
+
key="$(printf '%s' "${1:-default}" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')"
|
|
34
|
+
[ -n "$key" ] || key="DEFAULT"
|
|
35
|
+
printf 'PLANE_%s_API_KEY' "$key"
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
tp_cfg() {
|
|
28
39
|
[ -f "$ROLE_YAML" ] || return 0
|
|
29
40
|
python3 - "$ROLE_YAML" "$1" <<'PY'
|
|
@@ -54,13 +65,19 @@ else:
|
|
|
54
65
|
PY
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
# Board binding: .project.json (SOT) first, then role.yaml, then env.
|
|
68
|
+
# Board binding: .project.json (SOT) first, then legacy role.yaml, then env.
|
|
58
69
|
WS="$(pj_cfg workspace)"; [ -n "$WS" ] || WS="$(tp_cfg workspace)"; WS="${WS:-${PLANE_WORKSPACE:-}}"
|
|
59
|
-
PROJ="$(pj_cfg board_id)"; [ -n "$PROJ" ] || PROJ="$(tp_cfg project)"
|
|
70
|
+
PROJ="$(pj_cfg board_id)"; [ -n "$PROJ" ] || PROJ="$(tp_cfg project)"; [ -n "$PROJ" ] || PROJ="$(tp_cfg board_id)"
|
|
60
71
|
SM_IN_REVIEW="$(tp_cfg in_review)"; SM_IN_REVIEW="${SM_IN_REVIEW:-In Review}"
|
|
61
72
|
SM_DONE="$(tp_cfg completed)"; SM_DONE="${SM_DONE:-Done}"
|
|
62
73
|
API="$BASE/api/v1/workspaces/$WS"
|
|
63
74
|
|
|
75
|
+
if [ -z "${PLANE_API_KEY:-}" ]; then
|
|
76
|
+
KEY="$(workspace_key "$WS")"
|
|
77
|
+
eval "PLANE_API_KEY=\${$KEY:-}"
|
|
78
|
+
export PLANE_API_KEY
|
|
79
|
+
fi
|
|
80
|
+
|
|
64
81
|
# api METHOD PATH [JSON_BODY] — call Plane REST, print response body.
|
|
65
82
|
api() {
|
|
66
83
|
need_key
|
|
@@ -68,9 +85,12 @@ api() {
|
|
|
68
85
|
if [ -n "$body" ]; then
|
|
69
86
|
curl -fsS -X "$method" "$API/$path" \
|
|
70
87
|
-H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \
|
|
88
|
+
-H "User-Agent: curl/8.0" \
|
|
71
89
|
-d "$body"
|
|
72
90
|
else
|
|
73
|
-
curl -fsS -X "$method" "$API/$path"
|
|
91
|
+
curl -fsS -X "$method" "$API/$path" \
|
|
92
|
+
-H "X-API-Key: $PLANE_API_KEY" \
|
|
93
|
+
-H "User-Agent: curl/8.0"
|
|
74
94
|
fi
|
|
75
95
|
}
|
|
76
96
|
|
|
@@ -100,8 +120,8 @@ need_key
|
|
|
100
120
|
|
|
101
121
|
case "$OP" in
|
|
102
122
|
resolve)
|
|
103
|
-
[ -n "$WS" ] || die "workspace not set (
|
|
104
|
-
[ -n "$PROJ" ] || die "project not set (run 42-ticket-provider.sh)"
|
|
123
|
+
[ -n "$WS" ] || die "workspace not set (.project.json ticket_provider.workspace or PLANE_WORKSPACE)"
|
|
124
|
+
[ -n "$PROJ" ] || die "project not set (.project.json ticket_provider.board_id; run 42-ticket-provider.sh)"
|
|
105
125
|
printf '{"provider":"plane","board_id":"%s","board_url":"%s/%s/projects/%s/issues/"}\n' \
|
|
106
126
|
"$PROJ" "$BASE" "$WS" "$PROJ"
|
|
107
127
|
;;
|
|
@@ -21,35 +21,10 @@ profile: {{ agent_id }}
|
|
|
21
21
|
telegram:
|
|
22
22
|
bot_username: "{{ bot_handle }}"
|
|
23
23
|
|
|
24
|
-
# Ticket-provider
|
|
25
|
-
#
|
|
26
|
-
# swapping providers is a one-line change here. Filled in by 42-ticket-provider.sh.
|
|
24
|
+
# Ticket-provider adapter preference (plane | trello).
|
|
25
|
+
# Repo-owned board identity lives in repo-root .project.json, not in this role.
|
|
27
26
|
ticket_provider:
|
|
28
27
|
name: {{ ticket_provider }}
|
|
29
|
-
board_id: ""
|
|
30
|
-
board_url: ""
|
|
31
|
-
{%- if ticket_provider == 'plane' %}
|
|
32
|
-
workspace: "{{ plane_workspace }}"
|
|
33
|
-
project: "" # Plane project uuid (set by 42-ticket-provider.sh)
|
|
34
|
-
{%- elif ticket_provider == 'trello' %}
|
|
35
|
-
board: "" # Trello board id (set by 42-ticket-provider.sh)
|
|
36
|
-
{%- endif %}
|
|
37
|
-
# Optional normalized-state name overrides for non-standard boards.
|
|
38
|
-
in_review: "" # e.g. "In Review"
|
|
39
|
-
completed: "" # e.g. "Done"
|
|
40
|
-
|
|
41
|
-
# Board-reconciliation knobs for the heartbeat sentinel pass.
|
|
42
|
-
reconcile:
|
|
43
|
-
enabled: false # heartbeat runs the autonomous board-reconciliation pass ONLY when true; default off = checkpoint-only
|
|
44
|
-
grace_hours: 0 # 0 = no human-approval wait (adversarially-reviewed review-lane = done); set >0 to require a wait
|
|
45
|
-
auto_review: true # autonomous adversarial review (act, do not wait) on/off
|
|
46
|
-
|
|
47
|
-
# Plane metadata — retained for fleet-registry back-compat when provider=plane.
|
|
48
|
-
plane:
|
|
49
|
-
# Empty -> resolved from ~/.config/hermes-agent-template/config.toml [plane].workspace
|
|
50
|
-
workspace: "{{ plane_workspace }}"
|
|
51
|
-
# identifier is set by .scripts/42-ticket-provider.sh after creation (plane only)
|
|
52
|
-
identifier: ""
|
|
53
28
|
|
|
54
29
|
# Bloodbank wiring (consumed + produced subjects).
|
|
55
30
|
bloodbank:
|