@delorenj/pjangler 1.2.6 → 1.2.9
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 +54 -16
- package/dist/mcp-server.js +66 -17
- package/package.json +1 -1
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +12 -11
- package/templates/commonproject/copier.yml +24 -8
- package/templates/commonproject/mise.toml +3 -3
- package/templates/commonproject/template/.project.json.jinja +13 -6
- package/templates/hermes-agent/README.md +8 -8
- package/templates/hermes-agent/copier.yml +3 -4
- package/templates/hermes-agent/docs/architecture.md +4 -4
- package/templates/hermes-agent/docs/operations.md +2 -2
- package/templates/hermes-agent/docs/sentinel/README.md +6 -8
- package/templates/hermes-agent/docs/sentinel/architecture.md +2 -1
- package/templates/hermes-agent/docs/sentinel/development.md +13 -12
- package/templates/hermes-agent/docs/sentinel/providers.md +12 -32
- package/templates/hermes-agent/install-local.sh +6 -13
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +14 -37
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -0
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +6 -2
- 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 +5 -7
- package/templates/hermes-agent/template/SOUL.md.jinja +8 -7
- package/templates/hermes-agent/template/role.yaml.jinja +3 -6
- package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -51
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -176
|
@@ -21,17 +21,14 @@ profile: {{ agent_id }}
|
|
|
21
21
|
telegram:
|
|
22
22
|
bot_username: "{{ bot_handle }}"
|
|
23
23
|
|
|
24
|
-
# Ticket-provider binding (
|
|
24
|
+
# Ticket-provider binding (plane | trello).
|
|
25
25
|
# The heartbeat reconciliation pass talks ONLY to this via .scripts/lib/ticket-provider.sh;
|
|
26
26
|
# swapping providers is a one-line change here. Filled in by 42-ticket-provider.sh.
|
|
27
27
|
ticket_provider:
|
|
28
28
|
name: {{ ticket_provider }}
|
|
29
29
|
board_id: ""
|
|
30
30
|
board_url: ""
|
|
31
|
-
{%- if ticket_provider == '
|
|
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' %}
|
|
31
|
+
{%- if ticket_provider == 'plane' %}
|
|
35
32
|
workspace: "{{ plane_workspace }}"
|
|
36
33
|
project: "" # Plane project uuid (set by 42-ticket-provider.sh)
|
|
37
34
|
{%- elif ticket_provider == 'trello' %}
|
|
@@ -47,7 +44,7 @@ reconcile:
|
|
|
47
44
|
grace_hours: 0 # 0 = no human-approval wait (adversarially-reviewed review-lane = done); set >0 to require a wait
|
|
48
45
|
auto_review: true # autonomous adversarial review (act, do not wait) on/off
|
|
49
46
|
|
|
50
|
-
# Plane
|
|
47
|
+
# Plane metadata — retained for fleet-registry back-compat when provider=plane.
|
|
51
48
|
plane:
|
|
52
49
|
# Empty -> resolved from ~/.config/hermes-agent-template/config.toml [plane].workspace
|
|
53
50
|
workspace: "{{ plane_workspace }}"
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
|
@@ -1,176 +0,0 @@
|
|
|
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
|