@delorenj/pjangler 1.2.4 → 1.2.8
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 +797 -737
- package/dist/mcp-server.js +753 -683
- package/package.json +1 -1
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +12 -11
- package/templates/commonproject/copier.yml +58 -10
- package/templates/commonproject/mise.toml +3 -3
- package/templates/commonproject/template/.agents/hooks/sync.py +12 -1
- package/templates/commonproject/template/.agents/local.example.json +3 -1
- package/templates/commonproject/template/.project.json.jinja +13 -6
- package/templates/commonproject/template/mise.toml.jinja +8 -4
- 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 +2 -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 +6 -1
- package/templates/hermes-agent/template/SOUL.md.jinja +9 -3
- 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
|
@@ -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
|