@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,176 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Linear ticket-provider adapter (reference implementation).
|
|
3
|
+
#
|
|
4
|
+
# Credentials: LINEAR_API_KEY
|
|
5
|
+
# Board binding (role.yaml `ticket_provider:`):
|
|
6
|
+
# name: linear
|
|
7
|
+
# team: <TEAM_KEY> e.g. DEL
|
|
8
|
+
# project: "<Project name>" optional; scopes milestone/issue queries
|
|
9
|
+
# state_map: { in_review: "In Review", completed: "Done" } optional overrides
|
|
10
|
+
#
|
|
11
|
+
# Implements the contract in lib/ticket-provider.sh. All Linear access goes
|
|
12
|
+
# through GraphQL so the same envelope works in unattended runs.
|
|
13
|
+
set -eu
|
|
14
|
+
|
|
15
|
+
OP="${1:-}"; shift 2>/dev/null || true
|
|
16
|
+
ROLE_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
17
|
+
ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
18
|
+
|
|
19
|
+
die() { echo "linear: $*" >&2; exit 1; }
|
|
20
|
+
need_key() { [ -n "${LINEAR_API_KEY:-}" ] || die "LINEAR_API_KEY is not set"; }
|
|
21
|
+
|
|
22
|
+
# tp_cfg KEY — read ticket_provider.<KEY> from role.yaml (best-effort, flat).
|
|
23
|
+
tp_cfg() {
|
|
24
|
+
[ -f "$ROLE_YAML" ] || return 0
|
|
25
|
+
python3 - "$ROLE_YAML" "$1" <<'PY'
|
|
26
|
+
import sys, re, pathlib
|
|
27
|
+
text = pathlib.Path(sys.argv[1]).read_text()
|
|
28
|
+
m = re.search(r'(?ms)^ticket_provider:\s*$(.*?)(?=^\S)', text + "\n\x00")
|
|
29
|
+
block = m.group(1) if m else ""
|
|
30
|
+
key = sys.argv[2]
|
|
31
|
+
mm = re.search(rf'(?m)^\s*{re.escape(key)}:\s*"?([^"\n]*)"?\s*$', block)
|
|
32
|
+
print(mm.group(1).strip() if mm else "")
|
|
33
|
+
PY
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# pj_cfg KEY — read ticket_provider.<KEY> from the repo-root .project.json (the
|
|
37
|
+
# SOT), walking up from the role dir. Preferred over role.yaml.
|
|
38
|
+
pj_cfg() {
|
|
39
|
+
python3 - "$ROLE_DIR" "$1" <<'PY'
|
|
40
|
+
import sys, json, pathlib
|
|
41
|
+
start = pathlib.Path(sys.argv[1]).resolve(); key = sys.argv[2]
|
|
42
|
+
for parent in [start, *start.parents]:
|
|
43
|
+
f = parent / ".project.json"
|
|
44
|
+
if f.is_file():
|
|
45
|
+
try: tp = (json.loads(f.read_text()).get("ticket_provider") or {})
|
|
46
|
+
except Exception: tp = {}
|
|
47
|
+
print(tp.get(key, "") if isinstance(tp, dict) else ""); break
|
|
48
|
+
else:
|
|
49
|
+
print("")
|
|
50
|
+
PY
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# gql QUERY [VARS_JSON] — POST a GraphQL request, print data JSON, fail on errors.
|
|
54
|
+
gql() {
|
|
55
|
+
need_key
|
|
56
|
+
_vars="${2:-}"; [ -n "$_vars" ] || _vars='{}'
|
|
57
|
+
python3 - "$1" "$_vars" <<'PY'
|
|
58
|
+
import json, os, sys, urllib.request, urllib.error
|
|
59
|
+
q, variables = sys.argv[1], json.loads(sys.argv[2])
|
|
60
|
+
req = urllib.request.Request(
|
|
61
|
+
"https://api.linear.app/graphql",
|
|
62
|
+
data=json.dumps({"query": q, "variables": variables}).encode(),
|
|
63
|
+
headers={"Authorization": os.environ["LINEAR_API_KEY"],
|
|
64
|
+
"Content-Type": "application/json"},
|
|
65
|
+
method="POST")
|
|
66
|
+
try:
|
|
67
|
+
body = json.loads(urllib.request.urlopen(req, timeout=30).read())
|
|
68
|
+
except urllib.error.HTTPError as e:
|
|
69
|
+
body = json.loads(e.read() or "{}")
|
|
70
|
+
except urllib.error.URLError as e:
|
|
71
|
+
print(f"linear request failed: {e}", file=sys.stderr); sys.exit(1)
|
|
72
|
+
if body.get("errors"):
|
|
73
|
+
print(json.dumps(body["errors"]), file=sys.stderr); sys.exit(1)
|
|
74
|
+
print(json.dumps(body.get("data") or {}))
|
|
75
|
+
PY
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
TEAM="$(pj_cfg team)"; [ -n "$TEAM" ] || TEAM="$(tp_cfg team)"
|
|
79
|
+
PROJECT="$(pj_cfg project)"; [ -n "$PROJECT" ] || PROJECT="$(tp_cfg project)"
|
|
80
|
+
SM_IN_REVIEW="$(tp_cfg in_review)"; SM_IN_REVIEW="${SM_IN_REVIEW:-In Review}"
|
|
81
|
+
SM_DONE="$(tp_cfg completed)"; SM_DONE="${SM_DONE:-Done}"
|
|
82
|
+
|
|
83
|
+
# All Linear ops require the API key; fail fast and clean before any pipe.
|
|
84
|
+
need_key
|
|
85
|
+
|
|
86
|
+
case "$OP" in
|
|
87
|
+
resolve)
|
|
88
|
+
[ -n "$TEAM" ] || die "ticket_provider.team (Linear team key) not set in role.yaml"
|
|
89
|
+
gql 'query($k:String!){ teams(filter:{key:{eq:$k}}){nodes{id key name}} }' \
|
|
90
|
+
"$(printf '{"k":"%s"}' "$TEAM")" \
|
|
91
|
+
| python3 -c 'import sys,json; d=json.load(sys.stdin); t=(d.get("teams",{}).get("nodes") or [{}])[0]; print(json.dumps({"provider":"linear","board_id":t.get("id",""),"board_url":"https://linear.app/team/"+t.get("key","")}))'
|
|
92
|
+
;;
|
|
93
|
+
|
|
94
|
+
active_milestone)
|
|
95
|
+
# Linear project milestones; pick the first non-completed milestone in the project.
|
|
96
|
+
gql 'query($p:String){ projects(filter:{name:{eq:$p}}){nodes{ id name projectMilestones{nodes{id name targetDate}} state }} }' \
|
|
97
|
+
"$(printf '{"p":"%s"}' "$PROJECT")" \
|
|
98
|
+
| python3 -c 'import sys,json
|
|
99
|
+
d=json.load(sys.stdin); ps=d.get("projects",{}).get("nodes") or []
|
|
100
|
+
p=ps[0] if ps else {}
|
|
101
|
+
ms=(p.get("projectMilestones",{}) or {}).get("nodes") or []
|
|
102
|
+
m=ms[0] if ms else {"id":p.get("id",""),"name":p.get("name","")}
|
|
103
|
+
print(json.dumps({"id":m.get("id",""),"name":m.get("name",""),"state":p.get("state","")}))'
|
|
104
|
+
;;
|
|
105
|
+
|
|
106
|
+
list_issues)
|
|
107
|
+
[ -n "$TEAM" ] || die "ticket_provider.team not set"
|
|
108
|
+
gql 'query($k:String!){ issues(first:100, filter:{team:{key:{eq:$k}}}){nodes{ id identifier title updatedAt url state{name type} assignee{name} }} }' \
|
|
109
|
+
"$(printf '{"k":"%s"}' "$TEAM")" \
|
|
110
|
+
| python3 -c 'import sys,json
|
|
111
|
+
d=json.load(sys.stdin); out=[]
|
|
112
|
+
for n in d.get("issues",{}).get("nodes") or []:
|
|
113
|
+
st=n.get("state") or {}
|
|
114
|
+
out.append({"id":n["id"],"key":n.get("identifier",""),"title":n.get("title",""),
|
|
115
|
+
"state":st.get("name",""),"state_type":st.get("type",""),
|
|
116
|
+
"updated_at":n.get("updatedAt",""),
|
|
117
|
+
"assignee":(n.get("assignee") or {}).get("name",""),"url":n.get("url","")})
|
|
118
|
+
print(json.dumps(out))'
|
|
119
|
+
;;
|
|
120
|
+
|
|
121
|
+
get_issue)
|
|
122
|
+
ID="${1:?usage: get_issue <id>}"
|
|
123
|
+
gql 'query($id:String!){ issue(id:$id){ id identifier title description state{name type} comments{nodes{id body user{name}}} } }' \
|
|
124
|
+
"$(printf '{"id":"%s"}' "$ID")" \
|
|
125
|
+
| python3 -c 'import sys,json
|
|
126
|
+
d=json.load(sys.stdin); i=d.get("issue") or {}
|
|
127
|
+
st=i.get("state") or {}
|
|
128
|
+
cs=[{"id":c["id"],"body":c.get("body",""),"author":(c.get("user") or {}).get("name","")} for c in (i.get("comments",{}) or {}).get("nodes") or []]
|
|
129
|
+
print(json.dumps({"id":i.get("id",""),"key":i.get("identifier",""),"title":i.get("title",""),
|
|
130
|
+
"description":i.get("description",""),"acceptance":i.get("description",""),
|
|
131
|
+
"state":st.get("name",""),"state_type":st.get("type",""),"comments":cs}))'
|
|
132
|
+
;;
|
|
133
|
+
|
|
134
|
+
comment)
|
|
135
|
+
ID="${1:?usage: comment <id> <body>}"; BODY="${2:?usage: comment <id> <body>}"
|
|
136
|
+
gql 'mutation($id:String!,$b:String!){ commentCreate(input:{issueId:$id,body:$b}){ comment{id} success } }' \
|
|
137
|
+
"$(python3 -c 'import json,sys; print(json.dumps({"id":sys.argv[1],"b":sys.argv[2]}))' "$ID" "$BODY")" \
|
|
138
|
+
| python3 -c 'import sys,json; d=json.load(sys.stdin); print((d.get("commentCreate",{}).get("comment") or {}).get("id",""))'
|
|
139
|
+
;;
|
|
140
|
+
|
|
141
|
+
transition)
|
|
142
|
+
ID="${1:?usage: transition <id> <normalized-state>}"; TARGET="${2:?}"
|
|
143
|
+
# Map normalized -> a concrete Linear state name, then resolve its id on the team.
|
|
144
|
+
case "$TARGET" in
|
|
145
|
+
completed) WANT_TYPE=completed; WANT_NAME="$SM_DONE" ;;
|
|
146
|
+
in_review) WANT_TYPE=started; WANT_NAME="$SM_IN_REVIEW" ;;
|
|
147
|
+
started) WANT_TYPE=started; WANT_NAME="" ;;
|
|
148
|
+
unstarted) WANT_TYPE=unstarted; WANT_NAME="" ;;
|
|
149
|
+
backlog) WANT_TYPE=backlog; WANT_NAME="" ;;
|
|
150
|
+
*) die "invalid normalized state: $TARGET" ;;
|
|
151
|
+
esac
|
|
152
|
+
STATE_ID="$(gql 'query($id:String!){ issue(id:$id){ team{ states{nodes{id name type}} } } }' \
|
|
153
|
+
"$(printf '{"id":"%s"}' "$ID")" \
|
|
154
|
+
| WANT_TYPE="$WANT_TYPE" WANT_NAME="$WANT_NAME" python3 -c 'import sys,json,os
|
|
155
|
+
d=json.load(sys.stdin)
|
|
156
|
+
states=((d.get("issue") or {}).get("team") or {}).get("states",{}).get("nodes") or []
|
|
157
|
+
want_t=os.environ["WANT_TYPE"]; want_n=os.environ.get("WANT_NAME","")
|
|
158
|
+
named=[s for s in states if want_n and s["name"].lower()==want_n.lower()]
|
|
159
|
+
typed=[s for s in states if s.get("type")==want_t]
|
|
160
|
+
pick=(named or typed or [{}])[0]
|
|
161
|
+
print(pick.get("id",""))')"
|
|
162
|
+
[ -n "$STATE_ID" ] || die "no Linear state for normalized '$TARGET'"
|
|
163
|
+
gql 'mutation($id:String!,$s:String!){ issueUpdate(id:$id,input:{stateId:$s}){ success issue{identifier state{name}} } }' \
|
|
164
|
+
"$(python3 -c 'import json,sys; print(json.dumps({"id":sys.argv[1],"s":sys.argv[2]}))' "$ID" "$STATE_ID")" \
|
|
165
|
+
| python3 -c 'import sys,json; u=json.load(sys.stdin).get("issueUpdate",{});
|
|
166
|
+
print(("ok " + (u.get("issue") or {}).get("identifier","")) if u.get("success") else "FAILED"); sys.exit(0 if u.get("success") else 1)'
|
|
167
|
+
;;
|
|
168
|
+
|
|
169
|
+
create_board)
|
|
170
|
+
# Linear teams/projects are created by humans; the adapter resolves, not creates.
|
|
171
|
+
echo "linear: create_board is a no-op (Linear team/project created via Linear UI); using resolve" >&2
|
|
172
|
+
exec sh "$0" resolve
|
|
173
|
+
;;
|
|
174
|
+
|
|
175
|
+
*) die "unknown op: $OP" ;;
|
|
176
|
+
esac
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
2
|
# Plane ticket-provider adapter.
|
|
3
3
|
#
|
|
4
|
-
# Credentials: PLANE_API_KEY
|
|
4
|
+
# Credentials: PLANE_API_KEY (X-API-Key header)
|
|
5
5
|
# Endpoint: PLANE_BASE (default https://plane.delo.sh)
|
|
6
|
-
# Board binding (
|
|
6
|
+
# Board binding (role.yaml `ticket_provider:`):
|
|
7
|
+
# name: plane
|
|
7
8
|
# workspace: <workspace-slug> (or env PLANE_WORKSPACE)
|
|
8
|
-
#
|
|
9
|
+
# project: <project-uuid> (set by create_board / 42-ticket-provider)
|
|
9
10
|
# state_map: { in_review: "In Review", completed: "Done" } optional
|
|
10
11
|
#
|
|
11
12
|
# Plane model: project = board, cycle = milestone, state.group in
|
|
@@ -20,21 +21,9 @@ ROLE_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
|
20
21
|
ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
21
22
|
BASE="${PLANE_BASE:-https://plane.delo.sh}"
|
|
22
23
|
|
|
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
|
-
|
|
29
24
|
die() { echo "plane: $*" >&2; exit 1; }
|
|
30
25
|
need_key() { [ -n "${PLANE_API_KEY:-}" ] || die "PLANE_API_KEY is not set"; }
|
|
31
26
|
|
|
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
|
-
|
|
38
27
|
tp_cfg() {
|
|
39
28
|
[ -f "$ROLE_YAML" ] || return 0
|
|
40
29
|
python3 - "$ROLE_YAML" "$1" <<'PY'
|
|
@@ -65,19 +54,13 @@ else:
|
|
|
65
54
|
PY
|
|
66
55
|
}
|
|
67
56
|
|
|
68
|
-
# Board binding: .project.json (SOT) first, then
|
|
57
|
+
# Board binding: .project.json (SOT) first, then role.yaml, then env.
|
|
69
58
|
WS="$(pj_cfg workspace)"; [ -n "$WS" ] || WS="$(tp_cfg workspace)"; WS="${WS:-${PLANE_WORKSPACE:-}}"
|
|
70
|
-
PROJ="$(pj_cfg board_id)"; [ -n "$PROJ" ] || PROJ="$(tp_cfg project)"
|
|
59
|
+
PROJ="$(pj_cfg board_id)"; [ -n "$PROJ" ] || PROJ="$(tp_cfg project)"
|
|
71
60
|
SM_IN_REVIEW="$(tp_cfg in_review)"; SM_IN_REVIEW="${SM_IN_REVIEW:-In Review}"
|
|
72
61
|
SM_DONE="$(tp_cfg completed)"; SM_DONE="${SM_DONE:-Done}"
|
|
73
62
|
API="$BASE/api/v1/workspaces/$WS"
|
|
74
63
|
|
|
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
|
-
|
|
81
64
|
# api METHOD PATH [JSON_BODY] — call Plane REST, print response body.
|
|
82
65
|
api() {
|
|
83
66
|
need_key
|
|
@@ -85,12 +68,9 @@ api() {
|
|
|
85
68
|
if [ -n "$body" ]; then
|
|
86
69
|
curl -fsS -X "$method" "$API/$path" \
|
|
87
70
|
-H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \
|
|
88
|
-
-H "User-Agent: curl/8.0" \
|
|
89
71
|
-d "$body"
|
|
90
72
|
else
|
|
91
|
-
curl -fsS -X "$method" "$API/$path"
|
|
92
|
-
-H "X-API-Key: $PLANE_API_KEY" \
|
|
93
|
-
-H "User-Agent: curl/8.0"
|
|
73
|
+
curl -fsS -X "$method" "$API/$path" -H "X-API-Key: $PLANE_API_KEY"
|
|
94
74
|
fi
|
|
95
75
|
}
|
|
96
76
|
|
|
@@ -120,8 +100,8 @@ need_key
|
|
|
120
100
|
|
|
121
101
|
case "$OP" in
|
|
122
102
|
resolve)
|
|
123
|
-
[ -n "$WS" ] || die "workspace not set (.
|
|
124
|
-
[ -n "$PROJ" ] || die "project not set (
|
|
103
|
+
[ -n "$WS" ] || die "workspace not set (role.yaml ticket_provider.workspace or PLANE_WORKSPACE)"
|
|
104
|
+
[ -n "$PROJ" ] || die "project not set (run 42-ticket-provider.sh)"
|
|
125
105
|
printf '{"provider":"plane","board_id":"%s","board_url":"%s/%s/projects/%s/issues/"}\n' \
|
|
126
106
|
"$PROJ" "$BASE" "$WS" "$PROJ"
|
|
127
107
|
;;
|
package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md
CHANGED
|
@@ -13,7 +13,7 @@ actively tries to break the work, surface unmet acceptance criteria, hidden
|
|
|
13
13
|
regressions, and drift. On a clean adversarial verdict the loop treats the
|
|
14
14
|
ticket as done and moves on — through the ticket-provider adapter
|
|
15
15
|
(`tp transition <id> <state>`) — and emits a BloodBank decision event carrying
|
|
16
|
-
the full report. This works identically on Plane or Trello.
|
|
16
|
+
the full report. This works identically on Linear, Plane, or Trello.
|
|
17
17
|
|
|
18
18
|
The operator's verification is **deferred** to end-of-product QA over the review
|
|
19
19
|
lane, backed by a queryable decision trail. A downstream regression rollback is
|
|
@@ -144,7 +144,7 @@ Write `<ISSUE>.review.md`; the script validates it:
|
|
|
144
144
|
```markdown
|
|
145
145
|
# Autonomous Review Report: <ISSUE>
|
|
146
146
|
## Issue
|
|
147
|
-
-
|
|
147
|
+
- Linear/Plane/Trello issue: <ISSUE>
|
|
148
148
|
- Review lane reason:
|
|
149
149
|
## Reviewer
|
|
150
150
|
- Reviewer agent: <independent-agent-id>
|
package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md
CHANGED
|
@@ -19,7 +19,7 @@ approve merges.
|
|
|
19
19
|
All board access goes through the adapter (`tp`, from
|
|
20
20
|
`.scripts/lib/ticket-provider.sh`) and reasons in normalized states:
|
|
21
21
|
`backlog | unstarted | started | in_review | completed`. Never call the provider
|
|
22
|
-
directly — the engine is identical across Plane and Trello.
|
|
22
|
+
directly — the engine is identical across Linear, Plane, and Trello.
|
|
23
23
|
|
|
24
24
|
## Work-state feed
|
|
25
25
|
|
|
@@ -6,11 +6,6 @@ A cheap systemd heartbeat already decided this full pass is needed.
|
|
|
6
6
|
Working repo: the git root containing this role at `agents/hermes/{{ role }}/`.
|
|
7
7
|
Ticket provider: **{{ ticket_provider }}** (reached only through the adapter — see below).
|
|
8
8
|
|
|
9
|
-
The configured ticket provider is the authoritative repo ticket board/SOT for
|
|
10
|
-
this PM. Hermes local Kanban must NOT be an active durable task lifecycle
|
|
11
|
-
surface. Local Kanban may only run as an explicitly configured ephemeral child
|
|
12
|
-
execution queue and must never close/move authoritative repo tickets.
|
|
13
|
-
|
|
14
9
|
You are the **{{ target_repo }} PM**, running your continuous board-reconciliation
|
|
15
10
|
pass. Act autonomously, but stay inside the project contracts. Read
|
|
16
11
|
`agents/hermes/{{ role }}/SOUL.md` and the engine docs under
|
|
@@ -32,7 +27,7 @@ tp transition <id> <normalized-state> # backlog|unstarted|started|in_review|com
|
|
|
32
27
|
```
|
|
33
28
|
|
|
34
29
|
Reason in **normalized states**, not provider terms. This pass works identically
|
|
35
|
-
on Plane or Trello.
|
|
30
|
+
on Linear, Plane, or Trello.
|
|
36
31
|
|
|
37
32
|
## Pass
|
|
38
33
|
|
|
@@ -46,8 +46,8 @@ Envelope shape: CloudEvents 1.0, type `bloodbank.v1.<domain>.<entity>.<action>`,
|
|
|
46
46
|
`source = hermes://agent/{{ agent_id }}`. The consumer in `./runtime/` already
|
|
47
47
|
imports the envelope helper.
|
|
48
48
|
|
|
49
|
-
You **MUST NOT** invent new event `type` values.
|
|
50
|
-
|
|
49
|
+
You **MUST NOT** invent new event `type` values. Bloodbank owns the naming
|
|
50
|
+
contract at `~/code/33GOD/bloodbank/docs/event-naming.md` —
|
|
51
51
|
read it before publishing a type you haven't published before.
|
|
52
52
|
|
|
53
53
|
## Role-specific behavior
|
|
@@ -55,24 +55,22 @@ read it before publishing a type you haven't published before.
|
|
|
55
55
|
{% if role == "pm" -%}
|
|
56
56
|
You are the **project manager**. You triage incoming requests from Telegram /
|
|
57
57
|
Bloodbank command lanes, decompose them into discrete tasks on the
|
|
58
|
-
|
|
59
|
-
on `bloodbank.cmd.v1.agent.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
The configured ticket provider (`{{ ticket_provider }}`) is the authoritative
|
|
63
|
-
repo ticket board/SOT for this PM. Hermes local Kanban must NOT be an active
|
|
64
|
-
durable task lifecycle surface. The kanban toolset is removed from the PM's cli
|
|
65
|
-
surface. Local Kanban may only run as an explicitly configured ephemeral child
|
|
66
|
-
execution queue and must never close/move authoritative repo tickets.
|
|
58
|
+
Plane board, and route work to other agents in the fleet (e.g. the dev role
|
|
59
|
+
on `bloodbank.cmd.v1.agent.task.assign` with
|
|
60
|
+
`data.target_agent_id = {{ target_repo }}-dev`). You do not write application
|
|
61
|
+
code. You do not approve merges.
|
|
67
62
|
|
|
68
63
|
Default execution workflow for implementation delivery: use
|
|
69
|
-
`subagent-driven-development` in
|
|
64
|
+
`subagent-driven-development` in kanban-orchestrated codex mode
|
|
70
65
|
(WIP=1, spec review gate, quality review gate).
|
|
71
66
|
|
|
72
67
|
Decision events you commonly emit:
|
|
73
|
-
- `bloodbank.v1.repo.
|
|
74
|
-
- `bloodbank.v1.repo.
|
|
75
|
-
- `bloodbank.v1.repo.
|
|
68
|
+
- `bloodbank.v1.repo.decision.recorded`
|
|
69
|
+
- `bloodbank.v1.repo.intake.triaged`
|
|
70
|
+
- `bloodbank.v1.repo.task.created`
|
|
71
|
+
|
|
72
|
+
Put `repo = {{ target_repo }}` in event data; never insert repo or agent
|
|
73
|
+
identifiers into Bloodbank type or subject tokens.
|
|
76
74
|
|
|
77
75
|
Template-governor command contract:
|
|
78
76
|
- If operator says `update template to capture <X>`, run `hermes-pm-template-maintenance` workflow:
|
|
@@ -87,17 +85,17 @@ write tests, run the project's `mise run test` or equivalent, open PRs, and
|
|
|
87
85
|
respond to review comments. You do not merge — that's the reviewer's call.
|
|
88
86
|
|
|
89
87
|
Events you commonly emit:
|
|
90
|
-
- `bloodbank.v1.repo.
|
|
91
|
-
- `bloodbank.v1.repo.
|
|
92
|
-
- `bloodbank.v1.repo.
|
|
88
|
+
- `bloodbank.v1.repo.code.committed`
|
|
89
|
+
- `bloodbank.v1.repo.test.completed`
|
|
90
|
+
- `bloodbank.v1.repo.pr.opened`
|
|
93
91
|
{%- elif role == "review" -%}
|
|
94
92
|
You are the **reviewer**. You read PRs critically, check against the
|
|
95
93
|
project's CONTRIBUTING and AGENTS.md, and either approve or request changes.
|
|
96
94
|
You hold the merge gate for non-trivial changes.
|
|
97
95
|
|
|
98
96
|
Events:
|
|
99
|
-
- `bloodbank.v1.repo.
|
|
100
|
-
- `bloodbank.v1.repo.
|
|
97
|
+
- `bloodbank.v1.repo.review.completed`
|
|
98
|
+
- `bloodbank.v1.repo.pr.merged`
|
|
101
99
|
{%- else -%}
|
|
102
100
|
You operate as the **{{ role }}** agent for this repo. Define your contract
|
|
103
101
|
in this file (this section), then publish a `bloodbank.v1.agent.contract.
|
|
@@ -112,7 +110,7 @@ declared` event so the fleet knows what to route to you.
|
|
|
112
110
|
- **Hostnames**: Use `*.delo.sh` for external/cross-machine access (resolved
|
|
113
111
|
via Cloudflare Tunnel), `localhost` for same-host, Docker network service
|
|
114
112
|
names for container-to-container, Tailscale for private machine-to-machine.
|
|
115
|
-
- **
|
|
113
|
+
- **Plane**: Always include a Plane ticket reference in commit messages.
|
|
116
114
|
|
|
117
115
|
## Memory hygiene
|
|
118
116
|
|
|
@@ -21,16 +21,47 @@ profile: {{ agent_id }}
|
|
|
21
21
|
telegram:
|
|
22
22
|
bot_username: "{{ bot_handle }}"
|
|
23
23
|
|
|
24
|
-
# Ticket-provider
|
|
25
|
-
#
|
|
24
|
+
# Ticket-provider binding (linear | plane | trello).
|
|
25
|
+
# The heartbeat reconciliation pass talks ONLY to this via .scripts/lib/ticket-provider.sh;
|
|
26
|
+
# swapping providers is a one-line change here. Filled in by 42-ticket-provider.sh.
|
|
26
27
|
ticket_provider:
|
|
27
28
|
name: {{ ticket_provider }}
|
|
29
|
+
board_id: ""
|
|
30
|
+
board_url: ""
|
|
31
|
+
{%- if ticket_provider == 'linear' %}
|
|
32
|
+
team: "" # Linear team key, e.g. DEL (set before first sentinel run)
|
|
33
|
+
project: "" # optional Linear project name to scope milestones/issues
|
|
34
|
+
{%- elif ticket_provider == 'plane' %}
|
|
35
|
+
workspace: "{{ plane_workspace }}"
|
|
36
|
+
project: "" # Plane project uuid (set by 42-ticket-provider.sh)
|
|
37
|
+
{%- elif ticket_provider == 'trello' %}
|
|
38
|
+
board: "" # Trello board id (set by 42-ticket-provider.sh)
|
|
39
|
+
{%- endif %}
|
|
40
|
+
# Optional normalized-state name overrides for non-standard boards.
|
|
41
|
+
in_review: "" # e.g. "In Review"
|
|
42
|
+
completed: "" # e.g. "Done"
|
|
43
|
+
|
|
44
|
+
# Board-reconciliation knobs for the heartbeat sentinel pass.
|
|
45
|
+
reconcile:
|
|
46
|
+
enabled: false # heartbeat runs the autonomous board-reconciliation pass ONLY when true; default off = checkpoint-only
|
|
47
|
+
grace_hours: 0 # 0 = no human-approval wait (adversarially-reviewed review-lane = done); set >0 to require a wait
|
|
48
|
+
auto_review: true # autonomous adversarial review (act, do not wait) on/off
|
|
49
|
+
|
|
50
|
+
# Plane project (1:1 with this agent) — retained for fleet-registry back-compat.
|
|
51
|
+
plane:
|
|
52
|
+
# Empty -> resolved from ~/.config/hermes-agent-template/config.toml [plane].workspace
|
|
53
|
+
workspace: "{{ plane_workspace }}"
|
|
54
|
+
# identifier is set by .scripts/42-ticket-provider.sh after creation (plane only)
|
|
55
|
+
identifier: ""
|
|
28
56
|
|
|
29
57
|
# Bloodbank wiring (consumed + produced subjects).
|
|
30
58
|
bloodbank:
|
|
31
59
|
subscribe:
|
|
32
|
-
- "bloodbank.evt.v1.repo
|
|
33
|
-
- "bloodbank.cmd.v1.agent
|
|
60
|
+
- "bloodbank.evt.v1.repo.>"
|
|
61
|
+
- "bloodbank.cmd.v1.agent.>"
|
|
62
|
+
routing:
|
|
63
|
+
repo: "{{ target_repo }}"
|
|
64
|
+
target_agent_id: "{{ agent_id }}"
|
|
34
65
|
producer: "hermes-agent:{{ agent_id }}"
|
|
35
66
|
|
|
36
67
|
# Runtime repo (git-tracked HERMES_HOME, auto-checkpointed).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import types
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
12
|
+
CONSUMER_PATH = ROOT / "runtime-scaffold" / "bloodbank-consumer.py"
|
|
13
|
+
GENERATED_CONSUMER_PATH = ROOT / "template" / ".runtime-scaffold" / "bloodbank-consumer.py"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_consumer():
|
|
17
|
+
sys.modules.setdefault("nats", types.ModuleType("nats"))
|
|
18
|
+
spec = importlib.util.spec_from_file_location("bloodbank_consumer_contract", CONSUMER_PATH)
|
|
19
|
+
module = importlib.util.module_from_spec(spec)
|
|
20
|
+
spec.loader.exec_module(module)
|
|
21
|
+
module.AGENT_ID = "demo-pm"
|
|
22
|
+
module.REPO = "demo"
|
|
23
|
+
module.PRODUCER = "hermes-agent:demo-pm"
|
|
24
|
+
module.SOURCE = "hermes://agent/demo-pm"
|
|
25
|
+
return module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BloodbankConsumerContractTests(unittest.TestCase):
|
|
29
|
+
@classmethod
|
|
30
|
+
def setUpClass(cls):
|
|
31
|
+
cls.temp_home = tempfile.TemporaryDirectory()
|
|
32
|
+
cls.previous_home = os.environ.get("HERMES_HOME")
|
|
33
|
+
os.environ["HERMES_HOME"] = cls.temp_home.name
|
|
34
|
+
cls.consumer = load_consumer()
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def tearDownClass(cls):
|
|
38
|
+
if cls.previous_home is None:
|
|
39
|
+
os.environ.pop("HERMES_HOME", None)
|
|
40
|
+
else:
|
|
41
|
+
os.environ["HERMES_HOME"] = cls.previous_home
|
|
42
|
+
cls.temp_home.cleanup()
|
|
43
|
+
|
|
44
|
+
def test_scaffold_copies_stay_identical(self):
|
|
45
|
+
self.assertEqual(CONSUMER_PATH.read_bytes(), GENERATED_CONSUMER_PATH.read_bytes())
|
|
46
|
+
|
|
47
|
+
def test_subscriptions_use_fixed_canonical_routes(self):
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
self.consumer.SUBJECTS,
|
|
50
|
+
["bloodbank.evt.v1.repo.>", "bloodbank.cmd.v1.agent.>"],
|
|
51
|
+
)
|
|
52
|
+
self.assertNotIn("demo", ".".join(self.consumer.SUBJECTS))
|
|
53
|
+
self.assertNotIn("demo-pm", ".".join(self.consumer.SUBJECTS))
|
|
54
|
+
|
|
55
|
+
def test_envelope_keeps_identity_out_of_type_and_subject(self):
|
|
56
|
+
envelope = self.consumer.build_envelope(
|
|
57
|
+
"bloodbank.v1.repo.issue.updated",
|
|
58
|
+
{"repo": "demo", "issue": "PJAN-1"},
|
|
59
|
+
)
|
|
60
|
+
self.assertEqual(envelope["type"], "bloodbank.v1.repo.issue.updated")
|
|
61
|
+
self.assertEqual(envelope["subject"], "bloodbank.evt.v1.repo.issue.updated")
|
|
62
|
+
self.assertEqual(envelope["data"]["repo"], "demo")
|
|
63
|
+
self.assertEqual(envelope["actor"]["agent_id"], "demo-pm")
|
|
64
|
+
self.assertEqual(envelope["source"], "hermes://agent/demo-pm")
|
|
65
|
+
self.assertNotIn("demo", envelope["type"])
|
|
66
|
+
self.assertNotIn("demo", envelope["subject"])
|
|
67
|
+
with self.assertRaisesRegex(ValueError, r"bloodbank\.v1"):
|
|
68
|
+
self.consumer.build_envelope(
|
|
69
|
+
"bloodbank.v2.repo.issue.updated",
|
|
70
|
+
{"repo": "demo"},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def test_repo_events_route_by_data_repo(self):
|
|
74
|
+
subject = "bloodbank.evt.v1.repo.issue.updated"
|
|
75
|
+
envelope = self.consumer.build_envelope(
|
|
76
|
+
"bloodbank.v1.repo.issue.updated",
|
|
77
|
+
{"repo": "demo"},
|
|
78
|
+
)
|
|
79
|
+
self.assertTrue(self.consumer._is_for_consumer(subject, envelope))
|
|
80
|
+
envelope["data"]["repo"] = "another-repo"
|
|
81
|
+
self.assertFalse(self.consumer._is_for_consumer(subject, envelope))
|
|
82
|
+
|
|
83
|
+
def test_agent_commands_route_by_target_agent_id(self):
|
|
84
|
+
subject = "bloodbank.cmd.v1.agent.task.assign"
|
|
85
|
+
envelope = self.consumer.build_envelope(
|
|
86
|
+
"bloodbank.v1.agent.task.assign",
|
|
87
|
+
{"target_agent_id": "demo-pm"},
|
|
88
|
+
kind="command",
|
|
89
|
+
)
|
|
90
|
+
self.assertTrue(self.consumer._is_for_consumer(subject, envelope))
|
|
91
|
+
envelope["data"]["target_agent_id"] = "other-agent"
|
|
92
|
+
self.assertFalse(self.consumer._is_for_consumer(subject, envelope))
|
|
93
|
+
|
|
94
|
+
def test_rejects_identifier_bearing_or_mismatched_routes(self):
|
|
95
|
+
envelope = self.consumer.build_envelope(
|
|
96
|
+
"bloodbank.v1.repo.issue.updated",
|
|
97
|
+
{"repo": "demo"},
|
|
98
|
+
)
|
|
99
|
+
self.assertFalse(
|
|
100
|
+
self.consumer._is_for_consumer(
|
|
101
|
+
"bloodbank.evt.v1.repo.demo.issue.updated",
|
|
102
|
+
envelope,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
self.assertFalse(
|
|
106
|
+
self.consumer._is_for_consumer(
|
|
107
|
+
"bloodbank.evt.v1.repo.issue.created",
|
|
108
|
+
envelope,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
envelope["kind"] = "command"
|
|
112
|
+
self.assertFalse(
|
|
113
|
+
self.consumer._is_for_consumer(
|
|
114
|
+
"bloodbank.evt.v1.repo.issue.updated",
|
|
115
|
+
envelope,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def test_generated_contract_docs_do_not_put_identifiers_in_routes(self):
|
|
120
|
+
coupled_paths = [
|
|
121
|
+
ROOT / "docs" / "architecture.md",
|
|
122
|
+
ROOT / "docs" / "operations.md",
|
|
123
|
+
ROOT / "template" / "SOUL.md.jinja",
|
|
124
|
+
ROOT / "template" / "role.yaml.jinja",
|
|
125
|
+
ROOT / "runtime-scaffold" / "memories" / "MEMORY.md",
|
|
126
|
+
ROOT / "template" / ".runtime-scaffold" / "memories" / "MEMORY.md",
|
|
127
|
+
]
|
|
128
|
+
forbidden = re.compile(
|
|
129
|
+
r"bloodbank\.(?:v1\.repo|evt\.v1\.repo|cmd\.v1\.agent)\."
|
|
130
|
+
r"(?:\{\{|<repo>|<agent_id>)"
|
|
131
|
+
)
|
|
132
|
+
for path in coupled_paths:
|
|
133
|
+
with self.subTest(path=path):
|
|
134
|
+
self.assertIsNone(forbidden.search(path.read_text()))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
unittest.main()
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Bloodbank Gateway
|
|
2
|
-
|
|
3
|
-
Bloodbank command events should be a first-class Hermes gateway, not an inbox
|
|
4
|
-
directory that a later heartbeat drains.
|
|
5
|
-
|
|
6
|
-
## Goal
|
|
7
|
-
|
|
8
|
-
Make this true:
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
Telegram message -> Hermes gateway -> agent turn queue -> agent acts
|
|
12
|
-
Bloodbank command -> Hermes gateway -> agent turn queue -> agent acts
|
|
13
|
-
Web message -> Hermes gateway -> agent turn queue -> agent acts
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
The transport changes; the agent turn contract does not.
|
|
17
|
-
|
|
18
|
-
## Current State
|
|
19
|
-
|
|
20
|
-
`runtime-scaffold/bloodbank-consumer.py` subscribes to NATS subjects and writes
|
|
21
|
-
JSON files under `bloodbank-inbox/`. That proves subscription, envelope parsing,
|
|
22
|
-
and durable audit capture, but it is not command execution. It can accumulate
|
|
23
|
-
files while the agent never acts.
|
|
24
|
-
|
|
25
|
-
## Target Contract
|
|
26
|
-
|
|
27
|
-
- The Bloodbank gateway subscribes to `bloodbank.cmd.v1.agent.<agent_id>.>`.
|
|
28
|
-
- Each command event is converted into the same internal Hermes message/turn
|
|
29
|
-
shape used by Telegram and web gateways.
|
|
30
|
-
- Hermes owns backpressure: if the agent is busy, the command waits in the
|
|
31
|
-
message queue until the agent can act.
|
|
32
|
-
- The gateway emits lifecycle events:
|
|
33
|
-
- received
|
|
34
|
-
- accepted or rejected
|
|
35
|
-
- turn_started
|
|
36
|
-
- turn_completed or turn_failed
|
|
37
|
-
- Replies use the incoming `correlationid` and `reply_to`/reply subject when
|
|
38
|
-
present.
|
|
39
|
-
- Idempotency uses the CloudEvent `id`; duplicate events do not create duplicate
|
|
40
|
-
turns.
|
|
41
|
-
|
|
42
|
-
## Required Work
|
|
43
|
-
|
|
44
|
-
1. Add a Hermes transport adapter for Bloodbank/NATS.
|
|
45
|
-
2. Define a command payload schema that maps cleanly to a user prompt plus
|
|
46
|
-
metadata (`actor`, `source`, `priority`, `correlationid`, `reply_to`).
|
|
47
|
-
3. Replace the file-inbox consumer service with `hermes gateway bloodbank run`
|
|
48
|
-
or equivalent.
|
|
49
|
-
4. Keep optional audit persistence, but make it observational only.
|
|
50
|
-
5. Add smoke tests that publish a NATS command and assert that a Hermes turn is
|
|
51
|
-
created, processed, and acknowledged.
|
|
52
|
-
|
|
53
|
-
## Non-Goals
|
|
54
|
-
|
|
55
|
-
- No heartbeat inbox drain.
|
|
56
|
-
- No polling directory as the command execution mechanism.
|
|
57
|
-
- No per-agent one-off consumer logic.
|