@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.
Files changed (30) hide show
  1. package/dist/index.js +797 -737
  2. package/dist/mcp-server.js +753 -683
  3. package/package.json +1 -1
  4. package/templates/commonproject/AGENTS.md +3 -3
  5. package/templates/commonproject/README.md +12 -11
  6. package/templates/commonproject/copier.yml +58 -10
  7. package/templates/commonproject/mise.toml +3 -3
  8. package/templates/commonproject/template/.agents/hooks/sync.py +12 -1
  9. package/templates/commonproject/template/.agents/local.example.json +3 -1
  10. package/templates/commonproject/template/.project.json.jinja +13 -6
  11. package/templates/commonproject/template/mise.toml.jinja +8 -4
  12. package/templates/hermes-agent/README.md +8 -8
  13. package/templates/hermes-agent/copier.yml +3 -4
  14. package/templates/hermes-agent/docs/architecture.md +4 -4
  15. package/templates/hermes-agent/docs/operations.md +2 -2
  16. package/templates/hermes-agent/docs/sentinel/README.md +6 -8
  17. package/templates/hermes-agent/docs/sentinel/architecture.md +2 -1
  18. package/templates/hermes-agent/docs/sentinel/development.md +13 -12
  19. package/templates/hermes-agent/docs/sentinel/providers.md +12 -32
  20. package/templates/hermes-agent/install-local.sh +6 -13
  21. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +14 -37
  22. package/templates/hermes-agent/template/.scripts/70-systemd.sh +2 -0
  23. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +6 -2
  24. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
  25. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -1
  26. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +6 -1
  27. package/templates/hermes-agent/template/SOUL.md.jinja +9 -3
  28. package/templates/hermes-agent/template/role.yaml.jinja +3 -6
  29. package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -51
  30. 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