@delorenj/pjangler 1.2.17 → 1.2.19
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 +244 -6
- package/dist/mcp-server.js +245 -7
- package/package.json +8 -2
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +11 -12
- package/templates/commonproject/copier.yml +8 -27
- package/templates/commonproject/template/.project.json.jinja +9 -13
- package/templates/hermes-agent/README.md +9 -9
- package/templates/hermes-agent/config.example.toml +1 -66
- package/templates/hermes-agent/copier.yml +4 -3
- package/templates/hermes-agent/docs/architecture.md +9 -12
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/operations.md +6 -5
- package/templates/hermes-agent/docs/sentinel/README.md +9 -7
- package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
- package/templates/hermes-agent/docs/sentinel/development.md +12 -13
- package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
- package/templates/hermes-agent/install-local.sh +15 -14
- package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
- package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
- package/templates/hermes-agent/template/.gitignore.jinja +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
- package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
- package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +22 -66
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -1
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +1 -6
- package/templates/hermes-agent/template/SOUL.md.jinja +19 -21
- package/templates/hermes-agent/template/role.yaml.jinja +35 -4
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
- package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Create a Plane project in the configured workspace (one project per agent).
|
|
3
|
+
# Workspace + base URL come from role.yaml / config.toml (see _lib.sh).
|
|
4
|
+
# shellcheck source=_lib.sh
|
|
5
|
+
source "$(dirname "$0")/_lib.sh"
|
|
6
|
+
load_role_env
|
|
7
|
+
|
|
8
|
+
already_done 40-plane && { log "[40] plane already set up — skipping"; exit 0; }
|
|
9
|
+
[[ "${SKIP_PLANE:-0}" == "1" ]] && { log "[40] plane — SKIPPED"; mark_done 40-plane; exit 0; }
|
|
10
|
+
|
|
11
|
+
[[ -n "$PLANE_API_KEY" ]] || { warn "[40] PLANE_API_KEY not set; skipping. set PLANE_33GOD_API_KEY and re-run ./.scripts/40-plane.sh"; exit 0; }
|
|
12
|
+
|
|
13
|
+
# Build the 5-char identifier: <repo[0..2]><role[0..1]> uppercased
|
|
14
|
+
RAW=$(printf '%s%s' "${REPO:0:3}" "${ROLE:0:2}" | tr -cd '[:alnum:]' | tr '[:lower:]' '[:upper:]')
|
|
15
|
+
while (( ${#RAW} < 3 )); do RAW="${RAW}X"; done
|
|
16
|
+
IDENT="${RAW:0:5}"
|
|
17
|
+
|
|
18
|
+
# Project name = display_name (already smart-cased: "Bloodbank PM", "Hermes-Agent Dev").
|
|
19
|
+
# Plane forbids hyphens in names — substitute spaces.
|
|
20
|
+
NAME="${DISPLAY_NAME//-/ }"
|
|
21
|
+
|
|
22
|
+
log "[40] creating plane project '$NAME' [$IDENT]"
|
|
23
|
+
|
|
24
|
+
# Check for existing project with this identifier
|
|
25
|
+
EXISTING=$(curl -sS "$PLANE_BASE/api/v1/workspaces/$PLANE_WORKSPACE/projects/?per_page=200" \
|
|
26
|
+
-H "X-API-Key: $PLANE_API_KEY" \
|
|
27
|
+
| python3 -c "
|
|
28
|
+
import sys, json
|
|
29
|
+
d = json.load(sys.stdin)
|
|
30
|
+
ident = '$IDENT'
|
|
31
|
+
for p in d.get('results', []):
|
|
32
|
+
if (p.get('identifier') or '').upper() == ident:
|
|
33
|
+
print(p['id']); break")
|
|
34
|
+
|
|
35
|
+
if [[ -n "$EXISTING" ]]; then
|
|
36
|
+
log " plane project $IDENT already exists (id=$EXISTING) — reusing"
|
|
37
|
+
PROJECT_ID="$EXISTING"
|
|
38
|
+
else
|
|
39
|
+
RESP=$(curl -sS -X POST "$PLANE_BASE/api/v1/workspaces/$PLANE_WORKSPACE/projects/" \
|
|
40
|
+
-H "X-API-Key: $PLANE_API_KEY" \
|
|
41
|
+
-H "Content-Type: application/json" \
|
|
42
|
+
-d "$(python3 -c "import json,sys; print(json.dumps({'name': sys.argv[1], 'identifier': sys.argv[2], 'description': sys.argv[3]}))" \
|
|
43
|
+
"$NAME" "$IDENT" "Hermes agent board for $AGENT_ID")")
|
|
44
|
+
PROJECT_ID=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id') or '')")
|
|
45
|
+
[[ -n "$PROJECT_ID" ]] || die "plane create failed: $RESP"
|
|
46
|
+
log " plane project created id=$PROJECT_ID"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
yaml_set plane.identifier "$IDENT"
|
|
50
|
+
echo "$PROJECT_ID" > "$ROLE_DIR/.scripts/.plane-project-id"
|
|
51
|
+
mark_done 40-plane
|
|
@@ -10,12 +10,11 @@
|
|
|
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` and mirror the
|
|
14
|
+
# binding into role.yaml for back-compat (80-registry.sh / 99-summary.sh).
|
|
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"
|
|
19
18
|
# shellcheck source=lib/ticket-provider.sh
|
|
20
19
|
source "$(dirname "$0")/lib/ticket-provider.sh"
|
|
21
20
|
|
|
@@ -47,13 +46,13 @@ PY
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
# pj_write — merge board binding (optional) + this agent into .project.json.
|
|
50
|
-
# args: set_provider(0|1) provider board_id workspace identifier
|
|
49
|
+
# args: set_provider(0|1) provider board_id board_url workspace identifier team
|
|
51
50
|
pj_write() {
|
|
52
51
|
REPO="$REPO" REPO_ROOT="$REPO_ROOT" AGENT_ID="$AGENT_ID" ROLE="$ROLE" \
|
|
53
52
|
ROLE_DIR_REL="$ROLE_DIR_REL" PROJECT_DESC="${PROJECT_DESC:-}" \
|
|
54
53
|
python3 - "$PROJECT_JSON" "$@" <<'PY'
|
|
55
54
|
import sys, os, json, pathlib
|
|
56
|
-
(path, set_provider, provider, board_id, workspace, identifier) = sys.argv[1:
|
|
55
|
+
(path, set_provider, provider, board_id, board_url, workspace, identifier, team) = sys.argv[1:9]
|
|
57
56
|
p = pathlib.Path(path)
|
|
58
57
|
try:
|
|
59
58
|
d = json.loads(p.read_text())
|
|
@@ -72,30 +71,47 @@ if set_provider == "1":
|
|
|
72
71
|
if workspace: tp["workspace"] = workspace
|
|
73
72
|
if identifier: tp["identifier"] = identifier
|
|
74
73
|
if board_id: tp["board_id"] = board_id
|
|
75
|
-
tp
|
|
76
|
-
tp["
|
|
74
|
+
if board_url: tp["board_url"] = board_url
|
|
75
|
+
if team: tp["team"] = team
|
|
77
76
|
ag = d.setdefault("agents", {})
|
|
78
77
|
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})
|
|
81
78
|
p.write_text(json.dumps(d, indent=2) + "\n")
|
|
82
79
|
PY
|
|
83
80
|
log " .project.json updated (agent=$AGENT_ID)"
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
local provider="$1"
|
|
83
|
+
# Mirror the binding into role.yaml so legacy consumers keep working.
|
|
84
|
+
mirror_to_role_yaml() {
|
|
85
|
+
# mirror_to_role_yaml <provider> <board_id> <board_url> <workspace> <identifier> <team>
|
|
86
|
+
local provider="$1" bid="$2" burl="$3" ws="$4" ident="$5" team="$6"
|
|
90
87
|
yaml_set ticket_provider.name "$provider" 2>/dev/null || true
|
|
88
|
+
[ -n "$bid" ] && yaml_set ticket_provider.board_id "$bid" 2>/dev/null || true
|
|
89
|
+
[ -n "$burl" ] && yaml_set ticket_provider.board_url "$burl" 2>/dev/null || true
|
|
90
|
+
case "$provider" in
|
|
91
|
+
plane)
|
|
92
|
+
[ -n "$bid" ] && echo "$bid" > "$ROLE_DIR/.scripts/.plane-project-id"
|
|
93
|
+
[ -n "$ws" ] && yaml_set ticket_provider.workspace "$ws" 2>/dev/null || true
|
|
94
|
+
[ -n "$bid" ] && yaml_set ticket_provider.project "$bid" 2>/dev/null || true
|
|
95
|
+
[ -n "$ws" ] && yaml_set plane.workspace "$ws" 2>/dev/null || true
|
|
96
|
+
[ -n "$ident" ] && yaml_set plane.identifier "$ident" 2>/dev/null || true
|
|
97
|
+
;;
|
|
98
|
+
trello)
|
|
99
|
+
[ -n "$bid" ] && yaml_set ticket_provider.board "$bid" 2>/dev/null || true
|
|
100
|
+
;;
|
|
101
|
+
linear)
|
|
102
|
+
[ -n "$team" ] && yaml_set ticket_provider.team "$team" 2>/dev/null || true
|
|
103
|
+
;;
|
|
104
|
+
esac
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
# ── Provider resolution ──────────────────────────────────────────────────
|
|
94
108
|
# An existing repo board (in .project.json) wins — every agent binds to it.
|
|
95
109
|
SOT_TYPE="$(pj ticket_provider.type)"
|
|
96
110
|
SOT_BOARD_ID="$(pj ticket_provider.board_id)"
|
|
111
|
+
SOT_URL="$(pj ticket_provider.board_url)"
|
|
97
112
|
SOT_WS="$(pj ticket_provider.workspace)"
|
|
98
113
|
SOT_IDENT="$(pj ticket_provider.identifier)"
|
|
114
|
+
SOT_TEAM="$(pj ticket_provider.team)"
|
|
99
115
|
|
|
100
116
|
# role.yaml provider comes from copier --data (the operator's pjangler choice).
|
|
101
117
|
ROLE_PROVIDER="$(yaml_get ticket_provider.name)"
|
|
@@ -106,10 +122,9 @@ if [ -n "$SOT_BOARD_ID" ]; then
|
|
|
106
122
|
if [ -n "$ROLE_PROVIDER" ] && [ "$ROLE_PROVIDER" != "$PROVIDER" ]; then
|
|
107
123
|
warn "[42] requested provider '$ROLE_PROVIDER' but repo board is '$PROVIDER' (.project.json wins); binding to existing board"
|
|
108
124
|
fi
|
|
109
|
-
case "$PROVIDER" in plane|trello) ;; *) die "unsupported ticket provider in .project.json: $PROVIDER (expected plane|trello)" ;; esac
|
|
110
125
|
log "[42] binding $AGENT_ID to existing repo board (provider=$PROVIDER, id=$SOT_BOARD_ID)"
|
|
111
|
-
|
|
112
|
-
pj_write
|
|
126
|
+
mirror_to_role_yaml "$PROVIDER" "$SOT_BOARD_ID" "$SOT_URL" "$SOT_WS" "$SOT_IDENT" "$SOT_TEAM"
|
|
127
|
+
pj_write 0 "$PROVIDER" "" "" "" "" "" # register agent only; board already recorded
|
|
113
128
|
mark_done 42-ticket-provider
|
|
114
129
|
exit 0
|
|
115
130
|
fi
|
|
@@ -128,22 +143,45 @@ NAME="$(printf '%s' "$REPO" | tr '_-' ' ' | python3 -c 'import sys; print(" ".j
|
|
|
128
143
|
DESC="Ticket board for $REPO"
|
|
129
144
|
|
|
130
145
|
case "$PROVIDER" in
|
|
146
|
+
linear)
|
|
147
|
+
if [[ -z "${LINEAR_API_KEY:-}" ]]; then
|
|
148
|
+
warn "[42] LINEAR_API_KEY not set; set role.yaml/.project.json ticket_provider.team and re-run ./.scripts/42-ticket-provider.sh"
|
|
149
|
+
pj_write 1 linear "" "" "" "$IDENT" "$SOT_TEAM"
|
|
150
|
+
mark_done 42-ticket-provider; exit 0
|
|
151
|
+
fi
|
|
152
|
+
OUT="$(tp resolve 2>/dev/null || true)"
|
|
153
|
+
BID="$(printf '%s' "$OUT" | python3 -c 'import sys,json
|
|
154
|
+
try: print(json.load(sys.stdin).get("board_id",""))
|
|
155
|
+
except Exception: print("")')"
|
|
156
|
+
BURL="$(printf '%s' "$OUT" | python3 -c 'import sys,json
|
|
157
|
+
try: print(json.load(sys.stdin).get("board_url",""))
|
|
158
|
+
except Exception: print("")')"
|
|
159
|
+
if [ -n "$BID" ]; then
|
|
160
|
+
mirror_to_role_yaml linear "$BID" "$BURL" "" "$IDENT" "$SOT_TEAM"
|
|
161
|
+
pj_write 1 linear "$BID" "$BURL" "" "$IDENT" "$SOT_TEAM"
|
|
162
|
+
else
|
|
163
|
+
warn "[42] linear resolve returned no board; set ticket_provider.team and re-run"
|
|
164
|
+
pj_write 1 linear "" "" "" "$IDENT" "$SOT_TEAM"
|
|
165
|
+
fi
|
|
166
|
+
;;
|
|
167
|
+
|
|
131
168
|
plane|trello)
|
|
132
169
|
KEYVAR=PLANE_API_KEY; [ "$PROVIDER" = trello ] && KEYVAR=TRELLO_KEY
|
|
133
170
|
if [[ -z "${!KEYVAR:-}" ]]; then
|
|
134
171
|
warn "[42] $KEYVAR not set; skipping board creation. Set creds and re-run ./.scripts/42-ticket-provider.sh"
|
|
135
|
-
pj_write 1 "$PROVIDER" "" "${SOT_WS:-$
|
|
172
|
+
pj_write 1 "$PROVIDER" "" "" "${SOT_WS:-$PLANE_WORKSPACE}" "$IDENT" ""
|
|
136
173
|
mark_done 42-ticket-provider; exit 0
|
|
137
174
|
fi
|
|
138
175
|
OUT="$(tp create_board "$NAME" "$IDENT" "$DESC")" || die "create_board failed for $PROVIDER"
|
|
139
176
|
BID="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("board_id",""))')"
|
|
140
|
-
|
|
177
|
+
BURL="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("board_url",""))')"
|
|
178
|
+
WS="${SOT_WS:-$PLANE_WORKSPACE}"
|
|
141
179
|
[ "$PROVIDER" = trello ] && WS=""
|
|
142
|
-
|
|
143
|
-
pj_write 1 "$PROVIDER" "$BID" "$WS" "$IDENT"
|
|
180
|
+
mirror_to_role_yaml "$PROVIDER" "$BID" "$BURL" "$WS" "$IDENT" ""
|
|
181
|
+
pj_write 1 "$PROVIDER" "$BID" "$BURL" "$WS" "$IDENT" ""
|
|
144
182
|
;;
|
|
145
183
|
|
|
146
|
-
*) die "unknown ticket provider: $PROVIDER (expected plane|trello)" ;;
|
|
184
|
+
*) die "unknown ticket provider: $PROVIDER (expected linear|plane|trello)" ;;
|
|
147
185
|
esac
|
|
148
186
|
|
|
149
187
|
mark_done 42-ticket-provider
|
|
@@ -20,7 +20,8 @@ log "[60] installing bloodbank consumer for $AGENT_ID"
|
|
|
20
20
|
# placeholders. If $RUNTIME/bloodbank-consumer.py was ever lost (runtime
|
|
21
21
|
# submodule wiped, manual rm, etc.) and then someone restored it by
|
|
22
22
|
# copying from $RUNTIME_SCAFFOLD_DIR, they'd end up with an un-rendered
|
|
23
|
-
# consumer
|
|
23
|
+
# consumer whose envelope routing filter compares against literal `{{repo}}`
|
|
24
|
+
# and `{{agent_id}}` values.
|
|
24
25
|
# This block handles both failure modes idempotently.
|
|
25
26
|
CONSUMER="$RUNTIME/bloodbank-consumer.py"
|
|
26
27
|
SCAFFOLD_CONSUMER="$RUNTIME_SCAFFOLD_DIR/bloodbank-consumer.py"
|
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
source "$(dirname "$0")/_lib.sh"
|
|
6
6
|
load_role_env
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
log "[70] systemd already installed — skipping"
|
|
10
|
-
exit 0
|
|
11
|
-
fi
|
|
8
|
+
already_done 70-systemd && { log "[70] systemd already installed — skipping"; exit 0; }
|
|
12
9
|
[[ "${SKIP_SYSTEMD:-0}" == "1" ]] && { log "[70] systemd — SKIPPED"; mark_done 70-systemd; exit 0; }
|
|
13
10
|
|
|
14
11
|
RUNTIME="$ROLE_DIR/runtime"
|
|
@@ -35,9 +32,6 @@ Type=simple
|
|
|
35
32
|
Environment=HERMES_HOME=$RUNTIME
|
|
36
33
|
Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
37
34
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
38
|
-
Environment=HERMES_KANBAN_DISPATCH_IN_GATEWAY=0
|
|
39
|
-
Environment=HERMES_KANBAN_SLASH_ENABLED=0
|
|
40
|
-
EnvironmentFile=-$FLEET_ENV
|
|
41
35
|
EnvironmentFile=-$RUNTIME/.env
|
|
42
36
|
ExecStart=$HERMES_BIN gateway run --replace
|
|
43
37
|
Restart=on-failure
|
|
@@ -63,7 +57,6 @@ WorkingDirectory=$RUNTIME
|
|
|
63
57
|
Environment=HERMES_HOME=$RUNTIME
|
|
64
58
|
Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
65
59
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
66
|
-
EnvironmentFile=-$FLEET_ENV
|
|
67
60
|
EnvironmentFile=-$RUNTIME/.env
|
|
68
61
|
ExecStart=$HERMES_AGENT_REPO/.venv/bin/python $RUNTIME/bloodbank-consumer.py
|
|
69
62
|
Restart=on-failure
|
|
@@ -93,7 +86,6 @@ WorkingDirectory=$REPO_ROOT
|
|
|
93
86
|
Environment=HERMES_HOME=$RUNTIME
|
|
94
87
|
Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
95
88
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
96
|
-
EnvironmentFile=-$FLEET_ENV
|
|
97
89
|
EnvironmentFile=-%h/.config/hermes-agent/env
|
|
98
90
|
EnvironmentFile=-%h/.hermes/env
|
|
99
91
|
EnvironmentFile=-%h/.hermes/hermes-agent.env
|
|
@@ -120,7 +112,6 @@ UNIT
|
|
|
120
112
|
|
|
121
113
|
if systemd_user_available; then
|
|
122
114
|
systemctl --user daemon-reload
|
|
123
|
-
systemctl --user disable --now "hermes-${AGENT_ID}-checkpoint.timer" "hermes-${AGENT_ID}-checkpoint.service" >/dev/null 2>&1 || true
|
|
124
115
|
# `enable --now` both enables (persist across login) AND starts the unit now, so a
|
|
125
116
|
# freshly provisioned agent comes up live instead of dormant. Units with missing
|
|
126
117
|
# creds (e.g. a gateway with no Telegram token yet) fail softly via Restart=on-failure.
|
|
@@ -62,41 +62,6 @@ 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
|
-
|
|
100
65
|
# ─── Distributable config (~/.config/hermes-agent-template/config.toml) ──────
|
|
101
66
|
# Single source of truth for environment-specific defaults so this template can
|
|
102
67
|
# be handed to someone else without editing any script. Ship config.example.toml
|
|
@@ -147,10 +112,8 @@ load_role_env() {
|
|
|
147
112
|
BOT_HANDLE=$(yaml_get telegram.bot_username)
|
|
148
113
|
PROFILE_NAME=$(yaml_get profile)
|
|
149
114
|
|
|
150
|
-
# Plane workspace:
|
|
151
|
-
PLANE_WORKSPACE=$(
|
|
152
|
-
[[ -n "$PLANE_WORKSPACE" ]] || PLANE_WORKSPACE=$(yaml_get ticket_provider.workspace)
|
|
153
|
-
[[ -n "$PLANE_WORKSPACE" ]] || PLANE_WORKSPACE=$(yaml_get plane.workspace)
|
|
115
|
+
# Plane workspace: empty in role.yaml -> resolve from config.toml.
|
|
116
|
+
PLANE_WORKSPACE=$(yaml_get plane.workspace)
|
|
154
117
|
[[ -n "$PLANE_WORKSPACE" ]] || PLANE_WORKSPACE=$(config_get plane.workspace "33god")
|
|
155
118
|
|
|
156
119
|
# Runtime repo: role.yaml stores the bare repo name plus an optional owner.
|
|
@@ -166,27 +129,6 @@ load_role_env() {
|
|
|
166
129
|
PLANE_WORKSPACE RUNTIME_REPO PROFILE_NAME
|
|
167
130
|
}
|
|
168
131
|
|
|
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
|
-
|
|
190
132
|
# Skip a step if previously completed (idempotent reruns).
|
|
191
133
|
already_done() {
|
|
192
134
|
local marker="$ROLE_DIR/.scripts/.done-$1"
|
|
@@ -223,7 +165,7 @@ BLOODBANK_COMPOSE_DIR="${BLOODBANK_COMPOSE_DIR:-$(config_get bloodbank.compose_d
|
|
|
223
165
|
|
|
224
166
|
# Plane
|
|
225
167
|
PLANE_BASE="${PLANE_BASE:-$(config_get plane.base 'https://plane.delo.sh')}"
|
|
226
|
-
PLANE_API_KEY="${PLANE_API_KEY:-}"
|
|
168
|
+
PLANE_API_KEY="${PLANE_API_KEY:-${PLANE_33GOD_API_KEY:-}}"
|
|
227
169
|
|
|
228
170
|
export FLEET_ENV HERMES_BIN HERMES_AGENT_REPO HERMES_OAUTH_FILE CODEX_HOME \
|
|
229
171
|
RUNTIME_SCAFFOLD_DIR REGISTRY_FILE \
|
|
@@ -27,11 +27,6 @@ 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
|
-
]
|
|
35
30
|
# Voxxy plugin + service defaults for Hermes PM agents.
|
|
36
31
|
voxxy_plugin_dir = "~/code/voxxy/plugins/tts/voxxy"
|
|
37
32
|
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 (Plane | Trello) — never a
|
|
9
|
+
# through the ticket-provider adapter (Linear | 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,18 +19,12 @@ 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}"
|
|
23
22
|
LOG_FILE="$RUNTIME/logs/heartbeat.log"
|
|
24
23
|
CHECKPOINT_BIN="$ROLE_DIR/.scripts/checkpoint.sh"
|
|
25
24
|
CHECKPOINT_STAMP="$RUNTIME/.last-checkpoint"
|
|
26
25
|
|
|
27
|
-
if [[ -f "$FLEET_ENV" ]]; then
|
|
28
|
-
# shellcheck disable=SC1090
|
|
29
|
-
source "$FLEET_ENV"
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
26
|
# Hermes binary: explicit env > ~/.config/hermes-agent/hermes-bin > PATH.
|
|
33
|
-
HERMES_BIN="${HERMES_BIN
|
|
27
|
+
HERMES_BIN="${HERMES_BIN:-}"
|
|
34
28
|
if [[ -z "$HERMES_BIN" ]]; then
|
|
35
29
|
if [[ -r "$HOME/.config/hermes-agent/hermes-bin" ]]; then
|
|
36
30
|
HERMES_BIN="$(cat "$HOME/.config/hermes-agent/hermes-bin")"
|
|
@@ -86,61 +80,13 @@ print(value.strip().strip('"').strip("'"))
|
|
|
86
80
|
PYEOF
|
|
87
81
|
}
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
83
|
+
# True only when role.yaml has a reconcile: block with enabled: true. Block-aware
|
|
84
|
+
# so an unrelated `enabled:` leaf elsewhere in the file can't flip it on.
|
|
124
85
|
reconcile_enabled() {
|
|
125
|
-
python3 - "$
|
|
126
|
-
import
|
|
127
|
-
import re
|
|
128
|
-
import sys
|
|
86
|
+
python3 - "$ROLE_YAML" <<'PYEOF'
|
|
87
|
+
import re, sys
|
|
129
88
|
from pathlib import Path
|
|
130
|
-
|
|
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 = ""
|
|
89
|
+
text = Path(sys.argv[1]).read_text()
|
|
144
90
|
m = re.search(r'(?m)^reconcile:[ \t]*\n((?:[ \t]+\S.*\n?)*)', text)
|
|
145
91
|
block = m.group(1) if m else ""
|
|
146
92
|
me = re.search(r'(?m)^[ \t]+enabled:[ \t]*"?([A-Za-z]+)"?', block)
|
|
@@ -150,8 +96,17 @@ PYEOF
|
|
|
150
96
|
|
|
151
97
|
AGENT_ID="$(yaml_value agent_id)"
|
|
152
98
|
REPO_NAME="$(yaml_value repo)"
|
|
153
|
-
PROVIDER="$(
|
|
154
|
-
|
|
99
|
+
PROVIDER="$(yaml_block_value ticket_provider name)"
|
|
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)"
|
|
155
110
|
cd "$REPO_ROOT"
|
|
156
111
|
mkdir -p "$RUNTIME/logs"
|
|
157
112
|
|
|
@@ -177,10 +132,11 @@ else
|
|
|
177
132
|
fi
|
|
178
133
|
|
|
179
134
|
# Reconcile gate: the autonomous board-reconciliation pass runs only when
|
|
180
|
-
#
|
|
181
|
-
#
|
|
135
|
+
# role.yaml's reconcile.enabled is true. Default off → the heartbeat just
|
|
136
|
+
# checkpoints (behaves like the legacy hourly checkpoint timer). Flip
|
|
137
|
+
# reconcile.enabled to opt a repo into autonomous board reconciliation.
|
|
182
138
|
if [[ "$(reconcile_enabled)" != "true" ]]; then
|
|
183
|
-
printf '[heartbeat] reconcile disabled (
|
|
139
|
+
printf '[heartbeat] reconcile disabled (reconcile.enabled != true) — checkpoint-only tick\n'
|
|
184
140
|
maybe_checkpoint
|
|
185
141
|
exit 0
|
|
186
142
|
fi
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# shellcheck shell=bash
|
|
2
2
|
# Ticket-provider adapter dispatcher — the single seam between the heartbeat
|
|
3
|
-
# sentinel engine and a concrete ticket system (Plane | Trello).
|
|
3
|
+
# sentinel engine and a concrete ticket system (Linear | 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. Swapping providers is a one-line
|
|
7
|
+
# config change in role.yaml (ticket_provider.name) — no engine edits.
|
|
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 role.yaml under `ticket_provider:`.
|
|
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 /
|
|
@@ -64,7 +64,7 @@ PY
|
|
|
64
64
|
)"
|
|
65
65
|
[ -n "$name" ] && { printf '%s\n' "$name"; return 0; }
|
|
66
66
|
fi
|
|
67
|
-
printf '
|
|
67
|
+
printf 'linear\n'
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
# Directory holding provider implementations (sibling of this lib).
|
|
@@ -81,10 +81,6 @@ tp() {
|
|
|
81
81
|
|
|
82
82
|
local name impl
|
|
83
83
|
name="$(tp_provider_name)"
|
|
84
|
-
case "$name" in
|
|
85
|
-
plane|trello) ;;
|
|
86
|
-
*) echo "tp: unsupported ticket provider '$name' (expected plane|trello)" >&2; return 2 ;;
|
|
87
|
-
esac
|
|
88
84
|
impl="$(tp_providers_dir)/${name}.sh"
|
|
89
85
|
|
|
90
86
|
if [ ! -f "$impl" ]; then
|