@delorenj/pjangler 1.3.7 → 1.4.3

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 (60) hide show
  1. package/.mise/scripts/link-agentfiles.sh +38 -5
  2. package/README.md +20 -0
  3. package/dist/assets/project-notebook-skill/SHA256SUMS +10 -0
  4. package/dist/assets/project-notebook-skill/SKILL.md +64 -0
  5. package/dist/assets/project-notebook-skill/agents/openai.yaml +6 -0
  6. package/dist/assets/project-notebook-skill/export-manifest.json +56 -0
  7. package/dist/assets/project-notebook-skill/hooks/claude.settings.json +26 -0
  8. package/dist/assets/project-notebook-skill/hooks/hooks.master.json +26 -0
  9. package/dist/assets/project-notebook-skill/hooks/session-end.sh +228 -0
  10. package/dist/assets/project-notebook-skill/hooks/session-start.sh +228 -0
  11. package/dist/assets/project-notebook-skill/references/configuration.md +93 -0
  12. package/dist/assets/project-notebook-skill/references/recovery.md +54 -0
  13. package/dist/assets/project-notebook-skill/scripts/project-hooks.py +865 -0
  14. package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +848 -0
  15. package/dist/index.js +16982 -8532
  16. package/dist/index.js.map +7 -0
  17. package/dist/mcp-server.js +14107 -8089
  18. package/dist/mcp-server.js.map +7 -0
  19. package/dist/prompt.js +2 -1
  20. package/dist/prompt.js.map +7 -0
  21. package/package.json +9 -4
  22. package/templates/commonproject/copier.yml +14 -5
  23. package/templates/commonproject/template/.mise/scripts/link-agentfiles.sh +38 -5
  24. package/templates/commonproject/template/.mise/scripts/provision-packs.py +60 -2
  25. package/templates/commonproject/template/.mise/scripts/sync-skills.py +479 -24
  26. package/templates/commonproject/template/mise.toml.jinja +12 -6
  27. package/templates/hermes-agent/copier.yml +16 -3
  28. package/templates/hermes-agent/template/.gitignore.jinja +1 -0
  29. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +7 -4
  30. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +88 -94
  31. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +44 -21
  32. package/templates/hermes-agent/template/.scripts/30-telegram.sh +182 -171
  33. package/templates/hermes-agent/template/.scripts/31-slack.sh +260 -165
  34. package/templates/hermes-agent/template/.scripts/40-plane.sh +45 -36
  35. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +210 -41
  36. package/templates/hermes-agent/template/.scripts/70-systemd.sh +140 -22
  37. package/templates/hermes-agent/template/.scripts/80-registry.sh +42 -6
  38. package/templates/hermes-agent/template/.scripts/99-summary.sh +69 -16
  39. package/templates/hermes-agent/template/.scripts/_lib.sh +827 -10
  40. package/templates/hermes-agent/template/.scripts/channel-transaction.py +2340 -0
  41. package/templates/hermes-agent/template/.scripts/config.example.toml +8 -2
  42. package/templates/hermes-agent/template/.scripts/credential-launch.sh +5 -1
  43. package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -3
  44. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +29 -2
  45. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +182 -0
  46. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +108 -0
  47. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +9 -0
  48. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +546 -0
  49. package/templates/hermes-agent/template/.scripts/providers/linear.sh +24 -1
  50. package/templates/hermes-agent/template/.scripts/providers/plane.sh +72 -10
  51. package/templates/hermes-agent/template/.scripts/providers/trello.sh +25 -2
  52. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +5 -16
  53. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +2 -16
  54. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +13 -19
  55. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +29 -36
  56. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -8
  57. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +260 -0
  58. package/templates/hermes-agent/template/SOUL.md.jinja +14 -16
  59. package/templates/hermes-agent/template/hermes.jinja +1 -1
  60. package/templates/hermes-agent/template/role.yaml.jinja +11 -4
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env python3
2
+ """Store process-only secrets in 1Password and print op:// references.
3
+
4
+ Secret values are read from stdin and are never placed in argv, a temporary
5
+ file, stdout, or an ``op`` child environment. Item JSON travels only over an
6
+ anonymous pipe to the 1Password CLI. Credential pairs are staged in a new,
7
+ versioned item and both fields are verified before either reference is emitted.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import re
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ import uuid
19
+
20
+
21
+ def fail(message: str) -> "None":
22
+ raise SystemExit(f"1Password secret storage failed: {message}")
23
+
24
+
25
+ def op_run(args: list[str], *, payload: str | None = None) -> subprocess.CompletedProcess[str]:
26
+ allowed = (
27
+ "PATH",
28
+ "HOME",
29
+ "USERPROFILE",
30
+ "APPDATA",
31
+ "LOCALAPPDATA",
32
+ "SystemRoot",
33
+ "TMPDIR",
34
+ "TMP",
35
+ "TEMP",
36
+ "XDG_CONFIG_HOME",
37
+ "XDG_RUNTIME_DIR",
38
+ "OP_ACCOUNT",
39
+ "OP_CONNECT_HOST",
40
+ "OP_CONNECT_TOKEN",
41
+ "OP_LOAD_DESKTOP_APP_SETTINGS",
42
+ "OP_SERVICE_ACCOUNT_TOKEN",
43
+ )
44
+ env = {name: os.environ[name] for name in allowed if name in os.environ}
45
+ env.update(
46
+ (name, value)
47
+ for name, value in os.environ.items()
48
+ if name.startswith("OP_SESSION_")
49
+ )
50
+ env["NO_COLOR"] = "1"
51
+ return subprocess.run(
52
+ [OP, *args],
53
+ input=payload,
54
+ text=True,
55
+ stdout=subprocess.PIPE,
56
+ stderr=subprocess.PIPE,
57
+ check=False,
58
+ env=env,
59
+ timeout=30,
60
+ )
61
+
62
+
63
+ OP = shutil.which("op") or ""
64
+ if not OP:
65
+ fail("the op CLI is not installed")
66
+
67
+ if len(sys.argv) == 3 and sys.argv[1] == "--validate-reference":
68
+ reference = sys.argv[2]
69
+ if not reference.startswith("op://") or any(ch in reference for ch in "\r\n\0"):
70
+ fail("invalid 1Password reference")
71
+ resolved = op_run(["read", "--", reference])
72
+ if resolved.returncode != 0 or not resolved.stdout.rstrip("\n"):
73
+ fail("the configured reference did not resolve")
74
+ raise SystemExit(0)
75
+
76
+ if len(sys.argv) == 4 and sys.argv[1] == "--delete-item-id":
77
+ vault, item_id = sys.argv[2:]
78
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._ -]{0,126}", vault):
79
+ fail("vault name contains unsupported characters")
80
+ if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", item_id):
81
+ fail("invalid immutable item id")
82
+ deleted = op_run(["item", "delete", item_id, "--vault", vault, "--archive"])
83
+ if deleted.returncode != 0:
84
+ fail("staged item cleanup was rejected")
85
+ raise SystemExit(0)
86
+
87
+ pair_mode = len(sys.argv) == 6 and sys.argv[1] == "--store-pair"
88
+ single_staged_mode = len(sys.argv) == 5 and sys.argv[1] == "--store-staged"
89
+ staged_mode = pair_mode or single_staged_mode
90
+ if pair_mode:
91
+ vault, item, field_one, field_two = sys.argv[2:]
92
+ if field_one == field_two or not all(
93
+ re.fullmatch(r"[a-z][a-z0-9_]{0,63}", field)
94
+ for field in (field_one, field_two)
95
+ ):
96
+ fail("pair field names are invalid or not distinct")
97
+ elif single_staged_mode:
98
+ vault, item, field_one = sys.argv[2:]
99
+ if not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", field_one):
100
+ fail("staged field name is invalid")
101
+ else:
102
+ if len(sys.argv) != 3:
103
+ fail(
104
+ "usage: store-onepassword-secret.py <vault> <item>, "
105
+ "--store-staged <vault> <item-prefix> <field>, or "
106
+ "--store-pair <vault> <item-prefix> <field-one> <field-two>"
107
+ )
108
+ vault, item = sys.argv[1:]
109
+ safe = re.compile(r"[A-Za-z0-9][A-Za-z0-9._ -]{0,126}")
110
+ if not safe.fullmatch(vault) or not safe.fullmatch(item):
111
+ fail("vault or item name contains unsupported characters")
112
+
113
+ payload = sys.stdin.read()
114
+ if pair_mode:
115
+ values = payload.split("\n")
116
+ if len(values) != 2 or not all(values):
117
+ fail("credential pair input must contain exactly two non-empty lines")
118
+ secrets = [(field_one, values[0]), (field_two, values[1])]
119
+ elif single_staged_mode:
120
+ if payload.endswith("\n"):
121
+ payload = payload[:-1]
122
+ secrets = [(field_one, payload)]
123
+ else:
124
+ if payload.endswith("\n"):
125
+ payload = payload[:-1]
126
+ secrets = [("password", payload)]
127
+ if any(not value or any(ch in value for ch in "\r\n\0") for _, value in secrets):
128
+ fail("refusing to store an empty or multiline value")
129
+
130
+ if staged_mode:
131
+ if len(item) > 105:
132
+ fail("item prefix is too long for a versioned item")
133
+ item = f"{item}-v-{uuid.uuid4().hex[:16]}"
134
+ document = {
135
+ "title": item,
136
+ "category": "PASSWORD",
137
+ "fields": [
138
+ {
139
+ "id": field,
140
+ "type": "CONCEALED",
141
+ "label": field,
142
+ "value": value,
143
+ }
144
+ for field, value in secrets
145
+ ]
146
+ + [
147
+ {
148
+ "id": "notesPlain",
149
+ "type": "STRING",
150
+ "purpose": "NOTES",
151
+ "label": "notesPlain",
152
+ "value": (
153
+ "Managed by hermes-agent-template as a staged credential; "
154
+ "rotate through the provisioner."
155
+ ),
156
+ }
157
+ ],
158
+ }
159
+ stored = op_run(
160
+ ["item", "create", "--vault", vault, "--format=json", "-"],
161
+ payload=json.dumps(document),
162
+ )
163
+ if stored.returncode != 0:
164
+ fail("op rejected the staged credential update")
165
+ try:
166
+ created = json.loads(stored.stdout)
167
+ item_id = str(created.get("id") or "")
168
+ except (AttributeError, TypeError, json.JSONDecodeError):
169
+ fail("op item create returned invalid JSON")
170
+ if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", item_id):
171
+ fail("op item create omitted an immutable item id")
172
+ references = [f"op://{vault}/{item_id}/{field}" for field, _ in secrets]
173
+ for reference, (_, expected) in zip(references, secrets, strict=True):
174
+ verified = op_run(["read", "--", reference])
175
+ if verified.returncode != 0 or verified.stdout.rstrip("\n") != expected:
176
+ # The item is not active until the caller commits its refs. Archive
177
+ # this failed stage by immutable id; never guess/delete by title.
178
+ op_run(["item", "delete", item_id, "--vault", vault, "--archive"])
179
+ fail("a staged credential field did not verify; cleanup attempted")
180
+ print("\n".join([item_id, *references]))
181
+ raise SystemExit(0)
182
+
183
+ listed = op_run(["item", "list", "--vault", vault, "--format=json"])
184
+ if listed.returncode != 0:
185
+ fail("vault access/authentication was rejected")
186
+ try:
187
+ rows = json.loads(listed.stdout)
188
+ except (TypeError, json.JSONDecodeError):
189
+ fail("op item list returned invalid JSON")
190
+
191
+ matches = [row for row in rows if isinstance(row, dict) and row.get("title") == item]
192
+ if len(matches) > 1:
193
+ fail("multiple items have the requested title")
194
+
195
+ if matches:
196
+ item_id = str(matches[0].get("id") or "")
197
+ if not item_id:
198
+ fail("the existing item has no id")
199
+ fetched = op_run(["item", "get", item_id, "--vault", vault, "--format=json"])
200
+ if fetched.returncode != 0:
201
+ fail("the existing item could not be read")
202
+ try:
203
+ document = json.loads(fetched.stdout)
204
+ except (TypeError, json.JSONDecodeError):
205
+ fail("op item get returned invalid JSON")
206
+ fields = document.get("fields")
207
+ if not isinstance(fields, list):
208
+ fail("the existing item has no fields list")
209
+ concealed_field = next(
210
+ (field for field in fields if isinstance(field, dict) and field.get("id") == "password"),
211
+ None,
212
+ )
213
+ if concealed_field is None:
214
+ concealed_field = {
215
+ "id": "password",
216
+ "type": "CONCEALED",
217
+ "purpose": "PASSWORD",
218
+ "label": "password",
219
+ }
220
+ fields.append(concealed_field)
221
+ concealed_field["value"] = secrets[0][1]
222
+ stored = op_run(
223
+ ["item", "edit", item_id, "--vault", vault],
224
+ payload=json.dumps(document),
225
+ )
226
+ else:
227
+ document = {
228
+ "title": item,
229
+ "category": "PASSWORD",
230
+ "fields": [
231
+ {
232
+ "id": "password",
233
+ "type": "CONCEALED",
234
+ "purpose": "PASSWORD",
235
+ "label": "password",
236
+ "value": secrets[0][1],
237
+ },
238
+ {
239
+ "id": "notesPlain",
240
+ "type": "STRING",
241
+ "purpose": "NOTES",
242
+ "label": "notesPlain",
243
+ "value": "Managed by hermes-agent-template; rotate through the provisioner.",
244
+ },
245
+ ],
246
+ }
247
+ stored = op_run(
248
+ ["item", "create", "--vault", vault, "-"],
249
+ payload=json.dumps(document),
250
+ )
251
+
252
+ if stored.returncode != 0:
253
+ fail("op rejected the item update")
254
+
255
+ reference = f"op://{vault}/{item}/password"
256
+ verified = op_run(["read", "--", reference])
257
+ if verified.returncode != 0 or verified.stdout.rstrip("\n") != secrets[0][1]:
258
+ fail("the stored reference did not resolve to the supplied value")
259
+
260
+ print(reference)
@@ -39,7 +39,7 @@ question — not three vague ones.
39
39
 
40
40
  ## Default contract (every role)
41
41
 
42
- Envelope shape: CloudEvents 1.0, type `bloodbank.v1.<domain>.<entity>.<action>`,
42
+ Envelope shape: CloudEvents 1.0, type `bloodbank.<domain>.<entity>.<action>`,
43
43
  `actor.agent_id = {{ agent_id }}`, `producer = hermes-agent:{{ agent_id }}`,
44
44
  `source = hermes://agent/{{ agent_id }}`. Inbound commands arrive through the
45
45
  fleet-shared Hermes gateway and are routed by `data.target_agent_id`.
@@ -55,9 +55,8 @@ You are the **project-manager ORCHESTRATOR** — the autonomous Hermes carrier o
55
55
  Momo, and the twin of the human-drivable Momo. You share ONE board and ONE
56
56
  Hindsight bank with it; stay attributable and never split-brain the state. You
57
57
  triage incoming requests, decompose them into discrete tasks on the Plane
58
- board, and route work to other agents (e.g. the dev role on
59
- `bloodbank.cmd.v1.agent.task.assign` with
60
- `data.target_agent_id = {{ target_repo }}-dev`).
58
+ board, and route work to other agents (e.g. the `{{ target_repo }}-dev`
59
+ role).
61
60
 
62
61
  **Prime directives (non-negotiable):**
63
62
  - **Never mutate code** — every code change flows through a delegated worker.
@@ -75,9 +74,9 @@ Default execution workflow for implementation delivery: use
75
74
  (WIP=1, spec review gate, quality review gate).
76
75
 
77
76
  Decision events you commonly emit:
78
- - `bloodbank.v1.repo.decision.recorded`
79
- - `bloodbank.v1.repo.intake.triaged`
80
- - `bloodbank.v1.repo.task.created`
77
+ - `bloodbank.repo.decision.recorded`
78
+ - `bloodbank.repo.intake.triaged`
79
+ - `bloodbank.repo.task.created`
81
80
 
82
81
  Put `repo = {{ target_repo }}` in event data; never insert repo or agent
83
82
  identifiers into Bloodbank type or subject tokens.
@@ -94,18 +93,17 @@ You are the **developer**. You implement tasks dispatched by the PM agent,
94
93
  write tests, run the project's `mise run test` or equivalent, open PRs, and
95
94
  respond to review comments. You do not merge — that's the reviewer's call.
96
95
 
97
- Events you commonly emit:
98
- - `bloodbank.v1.repo.code.committed`
99
- - `bloodbank.v1.repo.test.completed`
100
- - `bloodbank.v1.repo.pr.opened`
96
+ You emit no Bloodbank event family of your own. Report progress on the ticket;
97
+ if a delivery fact is genuinely worth publishing, ask for a family to be minted
98
+ first — do not invent a type on the fly.
101
99
  {%- elif role == "review" -%}
102
100
  You are the **reviewer**. You read PRs critically, check against the
103
101
  project's CONTRIBUTING and AGENTS.md, and either approve or request changes.
104
102
  You hold the merge gate for non-trivial changes.
105
103
 
106
- Events:
107
- - `bloodbank.v1.repo.review.completed`
108
- - `bloodbank.v1.repo.pr.merged`
104
+ You emit no Bloodbank event family of your own. The review verdict lives on the
105
+ ticket and in the review report; if a review fact is genuinely worth publishing,
106
+ ask for a family to be minted first — do not invent a type on the fly.
109
107
  {%- elif role == "reporter" -%}
110
108
  You are the **DeLoNET company reporter**. Your single durable product is the
111
109
  operator's evidence-backed daily company rollup. Run the `delonet-daily-report`
@@ -135,8 +133,8 @@ credential is verified as owned by this reporter. Never reuse another agent's
135
133
  Telegram or Slack identity.
136
134
  {%- else -%}
137
135
  You operate as the **{{ role }}** agent for this repo. Define your contract
138
- in this file (this section), then publish a `bloodbank.v1.agent.contract.
139
- declared` event so the fleet knows what to route to you.
136
+ in this file (this section) this file is the fleet's record of what to
137
+ route to you.
140
138
  {%- endif %}
141
139
 
142
140
  ## DeloNet conventions you respect
@@ -42,7 +42,7 @@ if ! REPO_ROOT="$(git -C "$ROLE_DIR" rev-parse --show-toplevel 2>/dev/null)"; th
42
42
  fi
43
43
 
44
44
  # Never pass an identity-bearing chat credential from shared fleet state into
45
- # a profile process. Hermes loads the profile's own runtime/.env itself.
45
+ # a profile process. Hermes resolves the named profile's 1Password references.
46
46
  unset TELEGRAM_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN
47
47
 
48
48
  CONFIG="${HERMES_TEMPLATE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/hermes-agent-template/config.toml}"
@@ -30,8 +30,8 @@ telegram:
30
30
  bot_username: {{ bot_handle | tojson }}
31
31
  bot_id: ""
32
32
 
33
- # Slack is opt-in. Credentials live only in runtime/.env; this block records
34
- # safe provisioning state and the verified auth.test identity.
33
+ # Slack is opt-in. Credentials live in 1Password and the profile maps only
34
+ # op:// references; this block records safe state and auth.test identity.
35
35
  slack:
36
36
  provisioning_status: "deferred"
37
37
  team_id: ""
@@ -46,7 +46,6 @@ slack:
46
46
  ticket_provider:
47
47
  name: {{ ticket_provider | tojson }}
48
48
  board_id: ""
49
- board_url: ""
50
49
  {%- if ticket_provider == 'linear' %}
51
50
  team: "" # Linear team key, e.g. DEL (set before first sentinel run)
52
51
  project: "" # optional Linear project name to scope milestones/issues
@@ -63,7 +62,8 @@ ticket_provider:
63
62
 
64
63
  # Board-reconciliation knobs for the heartbeat sentinel pass.
65
64
  reconcile:
66
- enabled: false # heartbeat runs the autonomous board-reconciliation pass ONLY when true; default off = checkpoint-only
65
+ enabled: {{ reconcile_enabled | tojson }} # default on for PM operation; set false as the explicit checkpoint-only opt-out
66
+ explicit_opt_out: {% if reconcile_enabled %}false{% else %}true{% endif %} # distinguishes operator choice from the legacy default-off manifest
67
67
  grace_hours: 0 # 0 = no human-approval wait (adversarially-reviewed review-lane = done); set >0 to require a wait
68
68
  auto_review: true # autonomous adversarial review (act, do not wait) on/off
69
69
 
@@ -84,6 +84,13 @@ bloodbank:
84
84
  target_agent_id: {{ agent_id | tojson }}
85
85
  producer: {{ ("hermes-agent:" ~ agent_id) | tojson }}
86
86
 
87
+ # Reconciled by 70-systemd.sh. These are lifecycle observations, not desired
88
+ # state: gateway stays deferred until a dedicated chat credential is verified;
89
+ # heartbeat is independent and drives PM board reconciliation by default.
90
+ service_state:
91
+ gateway: "pending"
92
+ heartbeat: "pending"
93
+
87
94
  # Pure-local role-owned state. HERMES_HOME itself is the named profile directory
88
95
  # above; the legacy GitHub identity remains metadata-only for locating old
89
96
  # archives, and provisioning never creates or attaches a submodule.