@delorenj/pjangler 1.2.6 → 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 (27) hide show
  1. package/dist/index.js +54 -16
  2. package/dist/mcp-server.js +66 -17
  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 +24 -8
  7. package/templates/commonproject/mise.toml +3 -3
  8. package/templates/commonproject/template/.project.json.jinja +13 -6
  9. package/templates/hermes-agent/README.md +8 -8
  10. package/templates/hermes-agent/copier.yml +3 -4
  11. package/templates/hermes-agent/docs/architecture.md +4 -4
  12. package/templates/hermes-agent/docs/operations.md +2 -2
  13. package/templates/hermes-agent/docs/sentinel/README.md +6 -8
  14. package/templates/hermes-agent/docs/sentinel/architecture.md +2 -1
  15. package/templates/hermes-agent/docs/sentinel/development.md +13 -12
  16. package/templates/hermes-agent/docs/sentinel/providers.md +12 -32
  17. package/templates/hermes-agent/install-local.sh +6 -13
  18. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +14 -37
  19. package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -0
  20. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +6 -2
  21. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
  22. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -1
  23. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +5 -7
  24. package/templates/hermes-agent/template/SOUL.md.jinja +8 -7
  25. package/templates/hermes-agent/template/role.yaml.jinja +3 -6
  26. package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -51
  27. 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 (linear | plane | trello).
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 == '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' %}
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 project (1:1 with this agent) — retained for fleet-registry back-compat.
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