@delorenj/pjangler 1.2.18 → 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 -33
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +1 -27
- 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
|
|
|
@@ -73,35 +73,3 @@ The loop never ends a pass with work parked waiting on the operator.
|
|
|
73
73
|
prerequisite of the dependent and record the rollback.
|
|
74
74
|
|
|
75
75
|
Board status is not proof. Repository evidence and the close gate are proof.
|
|
76
|
-
|
|
77
|
-
## Post-loop improvement (end-of-batch retro)
|
|
78
|
-
|
|
79
|
-
When the board-clearing loop reaches a resting state — no ready or actionable
|
|
80
|
-
ticket to move and no live worker to monitor (a stop condition above, not a
|
|
81
|
-
mid-flight pause) — the sentinel runs a short end-of-batch retro before the pass
|
|
82
|
-
ends. Because it fires only at rest, it never competes with live ticket flow
|
|
83
|
-
(anti-stall).
|
|
84
|
-
|
|
85
|
-
Answer three questions about the batch just cleared:
|
|
86
|
-
|
|
87
|
-
1. What hurt this batch?
|
|
88
|
-
2. What should change?
|
|
89
|
-
3. Is the fix repo-local or external/template/fleet?
|
|
90
|
-
|
|
91
|
-
ALWAYS record the reflection as a run artifact under
|
|
92
|
-
`_bmad-output/implementation-artifacts/`: the three answers, any repo-local fix
|
|
93
|
-
applied, and a clear description of each proposed external/template/fleet
|
|
94
|
-
improvement. Nothing is silently dropped — the artifact is the durable record.
|
|
95
|
-
|
|
96
|
-
Then act on the answer — do not just note it:
|
|
97
|
-
|
|
98
|
-
- Repo-local, small, safe, and within this pass's scope → implement it now as a
|
|
99
|
-
minor follow-up. If it is larger or riskier, do not start it here; treat it as
|
|
100
|
-
external and surface it below.
|
|
101
|
-
- External / template / fleet (hermes-agent template, fleet config, another repo,
|
|
102
|
-
or an undecided product/architecture decision) → the adapter cannot open issues,
|
|
103
|
-
so surface it loudly instead: post a `tp comment` on the current tracking/active
|
|
104
|
-
issue proposing the follow-up, and flag the operator (the human-facing control
|
|
105
|
-
plane) to file it. Do not attempt to create a ticket through the adapter.
|
|
106
|
-
|
|
107
|
-
The retro is evidence like any other pass output.
|
|
@@ -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
|
|
|
@@ -96,26 +91,5 @@ on Plane or Trello.
|
|
|
96
91
|
`last_activity_at`, `log_path`).
|
|
97
92
|
10. Run or follow the session-end ritual; report board status, issues touched,
|
|
98
93
|
evidence touched, and the active worker issue or blocker.
|
|
99
|
-
11. **Post-loop improvement (end-of-batch retro).** ONLY when the board-clearing
|
|
100
|
-
loop reaches a resting state this pass — no ready/actionable ticket to move and
|
|
101
|
-
no live worker to monitor (idle, or only backlog / external-blocked work
|
|
102
|
-
remains). This is a retro AT REST: it never runs while a ready ticket or a
|
|
103
|
-
healthy worker exists, and it must not block or delay live ticket flow
|
|
104
|
-
(anti-stall, step 8). Answer three questions about the batch just cleared:
|
|
105
|
-
(1) What hurt this batch? (2) What should change? (3) Is the fix repo-local or
|
|
106
|
-
external/template/fleet? ALWAYS record the reflection as a run artifact
|
|
107
|
-
(evidence) under `_bmad-output/implementation-artifacts/`: the three answers,
|
|
108
|
-
any repo-local fix applied, and a clear description of each proposed
|
|
109
|
-
external/template/fleet improvement. Nothing is silently dropped — the artifact
|
|
110
|
-
is the durable record. Then act on the answer autonomously:
|
|
111
|
-
- **Repo-local** and small, safe, and within this pass's scope: implement it now
|
|
112
|
-
as a minor follow-up inside **{{ target_repo }}**. If it is larger or riskier
|
|
113
|
-
than that, do NOT start it here — treat it as external and surface it.
|
|
114
|
-
- **External / template / fleet** (hermes-agent template, fleet config, another
|
|
115
|
-
repo, or an undecided product/architecture decision): the adapter cannot open
|
|
116
|
-
issues, so surface it loudly instead — post a `tp comment` on the current
|
|
117
|
-
tracking/active issue proposing the follow-up, and flag the operator (the
|
|
118
|
-
human-facing control plane) to file it. Do not attempt to create a ticket
|
|
119
|
-
through the adapter.
|
|
120
94
|
|
|
121
95
|
Do not rely on a hard-coded seed ticket. Query the board every pass.
|
|
@@ -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()
|