@delorenj/pjangler 1.4.2 → 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 (39) hide show
  1. package/dist/index.js +3336 -1359
  2. package/dist/index.js.map +7 -0
  3. package/dist/mcp-server.js +1520 -838
  4. package/dist/mcp-server.js.map +7 -0
  5. package/dist/prompt.js +2 -1
  6. package/dist/prompt.js.map +7 -0
  7. package/package.json +8 -4
  8. package/templates/hermes-agent/copier.yml +16 -3
  9. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +7 -4
  10. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +88 -94
  11. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +44 -21
  12. package/templates/hermes-agent/template/.scripts/30-telegram.sh +182 -171
  13. package/templates/hermes-agent/template/.scripts/31-slack.sh +260 -165
  14. package/templates/hermes-agent/template/.scripts/40-plane.sh +45 -36
  15. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +210 -41
  16. package/templates/hermes-agent/template/.scripts/70-systemd.sh +129 -15
  17. package/templates/hermes-agent/template/.scripts/80-registry.sh +42 -6
  18. package/templates/hermes-agent/template/.scripts/99-summary.sh +69 -16
  19. package/templates/hermes-agent/template/.scripts/_lib.sh +773 -0
  20. package/templates/hermes-agent/template/.scripts/channel-transaction.py +2340 -0
  21. package/templates/hermes-agent/template/.scripts/config.example.toml +8 -2
  22. package/templates/hermes-agent/template/.scripts/credential-launch.sh +5 -1
  23. package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -3
  24. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +182 -0
  25. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +108 -0
  26. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +9 -0
  27. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +546 -0
  28. package/templates/hermes-agent/template/.scripts/providers/linear.sh +24 -1
  29. package/templates/hermes-agent/template/.scripts/providers/plane.sh +54 -8
  30. package/templates/hermes-agent/template/.scripts/providers/trello.sh +25 -2
  31. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +5 -16
  32. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +2 -16
  33. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +13 -19
  34. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +29 -36
  35. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -8
  36. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +260 -0
  37. package/templates/hermes-agent/template/SOUL.md.jinja +14 -16
  38. package/templates/hermes-agent/template/hermes.jinja +1 -1
  39. package/templates/hermes-agent/template/role.yaml.jinja +11 -4
@@ -144,6 +144,25 @@ print(json.dumps({"id":c.get("id",""),"key":c.get("id",""),"title":c.get("name",
144
144
  api PUT "cards/$ID" "idList=$LID" | python3 -c 'import sys,json; c=json.load(sys.stdin); print("ok "+c.get("id",""))'
145
145
  ;;
146
146
 
147
+ describe_board)
148
+ # Read-only board lookup against an EXPLICIT workspace argument, mirroring
149
+ # the Plane op, so no ambient binding can send the query somewhere else.
150
+ # Trello mints NO project key, so `identifier` is ALWAYS empty here: an
151
+ # empty identifier is this provider's authoritative answer, not a lookup
152
+ # failure, and callers must never promote it to a confirmed value.
153
+ DWS="${1:?usage: describe_board <workspace> <board_id>}"
154
+ DBID="${2:?usage: describe_board <workspace> <board_id>}"
155
+ api GET "boards/$DBID" "fields=id,name,idOrganization" \
156
+ | WS="$DWS" BID="$DBID" python3 -c 'import sys, json, os
157
+ b = json.load(sys.stdin)
158
+ print(json.dumps({
159
+ "board_id": str(b.get("id") or os.environ["BID"]),
160
+ "identifier": "",
161
+ "workspace": str(b.get("idOrganization") or os.environ["WS"]),
162
+ "name": str(b.get("name") or ""),
163
+ }))'
164
+ ;;
165
+
147
166
  create_board)
148
167
  NAME="${1:?usage: create_board <name> <ident> <desc>}"
149
168
  EXIST="$(api GET "members/me/boards" "fields=name" | NM="$NAME" python3 -c 'import sys,json,os
@@ -154,8 +173,12 @@ print(next((b["id"] for b in rows if b.get("name","").lower()==nm), ""))')"
154
173
  | python3 -c 'import sys,json; print(json.load(sys.stdin).get("id",""))')"
155
174
  fi
156
175
  [ -n "$BID" ] || die "create_board failed"
157
- api GET "boards/$BID" "fields=url" | BID="$BID" python3 -c 'import sys,json,os
158
- b=json.load(sys.stdin); print(json.dumps({"board_id":os.environ["BID"],"board_url":b.get("url","")}))'
176
+ # Trello assigns no project key, so the caller's prefix IS the identity and
177
+ # this adapter is the only thing that can confirm it. Echo it back: the
178
+ # create_board envelope always carries the identifier the board is bound
179
+ # under, so no caller ever has to invent one.
180
+ api GET "boards/$BID" "fields=url" | BID="$BID" IDENT="${2:-}" python3 -c 'import sys,json,os
181
+ b=json.load(sys.stdin); print(json.dumps({"board_id":os.environ["BID"],"board_url":b.get("url",""),"identifier":os.environ.get("IDENT","")}))'
159
182
  ;;
160
183
 
161
184
  create_issue)
@@ -2,8 +2,8 @@
2
2
  # Provider-agnostic autonomous adversarial-review decision gate (act, don't wait).
3
3
  #
4
4
  # The rigorous, INDEPENDENT ADVERSARIAL review is the normal per-pass path: an
5
- # adversarial microscope that couples the close gate, an independent-reviewer
6
- # drift attestation, and the BloodBank decision event. A clean adversarial
5
+ # adversarial microscope that couples the close gate with an independent-reviewer
6
+ # drift attestation. A clean adversarial
7
7
  # verdict is acted on AUTONOMOUSLY -- the loop treats the ticket as done and
8
8
  # moves on; it never parks the ticket waiting on the operator for approval or
9
9
  # sign-off. Every adversarial check stays at full strength (locked-intent
@@ -40,17 +40,14 @@ ROLE_YAML="$ROLE_DIR/role.yaml"
40
40
  ROOT="$(git -C "$ROLE_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)"
41
41
  cd "$ROOT"
42
42
 
43
- EMIT="$BIN_DIR/emit-event.py"
44
43
  CLOSE_GATE="$BIN_DIR/issue-close-gate.sh"
45
44
  EVIDENCE="_bmad-output/implementation-artifacts/issue-evidence/$ISSUE.md"
46
45
 
47
46
  yget() { sed -n "s/^[[:space:]]*$1:[[:space:]]*//p" "$ROLE_YAML" 2>/dev/null | head -n1 | tr -d '"' | tr -d '\r'; }
48
- REPO="$(yget repo)"; REPO="${REPO:-unknown}"
49
- # Informational only: recorded in the decision event/comment, never a blocking
50
- # wait. Default 0 (no grace); an operator may set grace_hours>0 to reintroduce one.
47
+ # Informational only: recorded in the ticket comment, never a blocking wait.
48
+ # Default 0 (no grace); an operator may set grace_hours>0 to reintroduce one.
51
49
  GRACE_HOURS="${RECONCILE_GRACE_HOURS:-$(yget grace_hours)}"; GRACE_HOURS="${GRACE_HOURS:-0}"
52
50
  AUTO="$(yget auto_review)"; AUTO="${AUTO:-true}"
53
- EVT="bloodbank.v1.repo.$REPO.issue.autonomous_review.decided"
54
51
 
55
52
  if [[ "${RECONCILE_AUTO_REVIEW:-$AUTO}" == "false" || "${RECONCILE_AUTO_REVIEW:-}" == "off" ]]; then
56
53
  printf 'Autonomous review is disabled (reconcile.auto_review=false).\n' >&2; exit 3
@@ -85,14 +82,6 @@ if sh "$CLOSE_GATE" "$ISSUE" "$ROOT" >/dev/null 2>&1 </dev/null; then GATE=pass;
85
82
 
86
83
  if [[ -n "$HOLD" ]]; then DECISION=held; else DECISION=accepted; fi
87
84
 
88
- python3 "$EMIT" "$EVT" --root "$ROOT" \
89
- --source "repo://sentinel/bin/issue-autonomous-review.sh" --actor-id "$REVIEWER" \
90
- --field issue="$ISSUE" --field decision="$DECISION" --field drift="$DRIFT" \
91
- --field close_gate="$GATE" --field reviewer_agent="$REVIEWER" \
92
- --field evidence_file="$EVIDENCE" --field report_file="$REPORT" \
93
- --field grace_hours="$GRACE_HOURS" --field hold_reasons="${HOLD:-none}" \
94
- --quiet </dev/null || printf 'WARN: decision event emission failed; event trail incomplete.\n' >&2
95
-
96
85
  if [[ "$DECISION" == "held" ]]; then
97
86
  printf 'AUTONOMOUS REVIEW: HOLD for %s\nReasons: %s\n' "$ISSUE" "$HOLD" >&2
98
87
  exit 3
@@ -109,7 +98,7 @@ if [[ "$CLOSE" -eq 1 ]]; then
109
98
  TICKET_PROVIDER="$PROV" bash -c '. "$1"; tp comment "$2" "$3"' _ "$SCRIPTS_DIR/lib/ticket-provider.sh" "$ISSUE" \
110
99
  "Autonomously accepted by $REVIEWER under the independent adversarial-review protocol (drift: $DRIFT, gate: $GATE, grace ${GRACE_HOURS}h informational). Treated as done; review report: $REPORT." >/dev/null 2>&1 || true
111
100
  else
112
- printf 'Adapter transition failed; decision event recorded, issue left open.\n' >&2
101
+ printf 'Adapter transition failed; issue left open.\n' >&2
113
102
  exit 1
114
103
  fi
115
104
  else
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env sh
2
2
  # Provider-agnostic close gate. Verifies an issue's evidence file is complete
3
- # before any closure (manual or autonomous). Repo name is read from role.yaml so
4
- # event types carry the right repo lane.
3
+ # before any closure (manual or autonomous). Pure gate: it reports PASS/FAIL on
4
+ # stdout/stderr and via the exit code, and publishes nothing.
5
5
  #
6
6
  # Usage: issue-close-gate.sh ISSUE_ID [REPO_ROOT]
7
7
  set -eu
@@ -13,17 +13,9 @@ fi
13
13
  ISSUE="$1"
14
14
  case "$ISSUE" in *[!A-Za-z0-9_-]*) printf 'Invalid issue id: %s\n' "$ISSUE" >&2; exit 2 ;; esac
15
15
 
16
- BIN_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
17
- ROLE_DIR="$(cd "$BIN_DIR/../../.." && pwd)"
18
- ROLE_YAML="$ROLE_DIR/role.yaml"
19
- EMIT="$BIN_DIR/emit-event.py"
20
16
  ROOT="${2:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
21
17
  cd "$ROOT"
22
18
 
23
- REPO="$(sed -n 's/^repo:[[:space:]]*//p' "$ROLE_YAML" 2>/dev/null | head -n1 | tr -d '"' | tr -d '\r')"
24
- REPO="${REPO:-unknown}"
25
- EVT_PREFIX="bloodbank.v1.repo.$REPO.issue"
26
-
27
19
  FILE="_bmad-output/implementation-artifacts/issue-evidence/$ISSUE.md"
28
20
  FAIL=0
29
21
  check() { grep -q "$1" "$FILE" || { printf 'Missing required evidence: %s\n' "$2" >&2; FAIL=1; }; }
@@ -49,14 +41,8 @@ grep -q 'Ledger updated: yes' "$FILE" || { printf 'Ledger update is not marked y
49
41
  grep -q 'Close recommendation: ready' "$FILE" || { printf 'Close recommendation is not ready.\n' >&2; FAIL=1; }
50
42
 
51
43
  if [ "$FAIL" -ne 0 ]; then
52
- python3 "$EMIT" "$EVT_PREFIX.gate.failed" --root "$ROOT" \
53
- --source "repo://sentinel/bin/issue-close-gate.sh" \
54
- --field issue="$ISSUE" --field evidence_file="$FILE" --quiet </dev/null || true
55
44
  printf '\nCLOSE GATE: FAIL for %s\n' "$ISSUE" >&2
56
45
  exit 1
57
46
  fi
58
47
 
59
- python3 "$EMIT" "$EVT_PREFIX.gate.passed" --root "$ROOT" \
60
- --source "repo://sentinel/bin/issue-close-gate.sh" \
61
- --field issue="$ISSUE" --field evidence_file="$FILE" --quiet </dev/null || true
62
48
  printf 'CLOSE GATE: PASS for %s\n' "$ISSUE"
@@ -112,29 +112,23 @@ loop:
112
112
  .scripts/sentinel/bin/issue-autonomous-review.sh <ISSUE> <REPORT> --close
113
113
  ```
114
114
 
115
- ## Decision event
115
+ ## Decision record
116
116
 
117
- ```text
118
- bloodbank.v1.repo.<repo>.issue.autonomous_review.decided
119
- ```
120
-
121
- Required data: `issue`, `decision` (`accepted | held`), `drift`, `close_gate`,
122
- `reviewer_agent`, `evidence_file`, `report_file`. See `bloodbank-events.md`.
117
+ The verdict is carried by the script's exit code (`0` accepted, `3` held) and
118
+ its stdout/stderr, plus the review report and the ticket comment it posts. The
119
+ durable accountability trail is the review report + evidence file in the repo.
120
+ No event is published; see `bloodbank-events.md` for why the old
121
+ `repo.issue.*` family was retired.
123
122
 
124
123
  ## Downstream regression rollback
125
124
 
126
125
  Deferring operator QA means a later dependent can prove a review-accepted
127
126
  feature is ACTUALLY BROKEN. When that happens, move the accepted ticket back to
128
127
  active (`started` if a worker takes it now, else `unstarted`) as a PREREQUISITE
129
- of the dependent, via the adapter (`tp transition <id> <state>`); comment naming
130
- the dependent + symptom; and emit:
131
-
132
- ```text
133
- bloodbank.v1.repo.<repo>.issue.review_rollback.recorded
134
- ```
135
-
136
- Required data: `issue`, `surfaced_by`, `reason`. The dependent is blocked on the
137
- prerequisite until it is fixed. This is expected and healthy — it is the trade
128
+ of the dependent, via the adapter (`tp transition <id> <state>`), and comment
129
+ naming the dependent + symptom. Record the rollback in the ticket comment and
130
+ the issue evidence file. The dependent is blocked on the prerequisite until it
131
+ is fixed. This is expected and healthy — it is the trade
138
132
  for deferring operator QA, not a failure.
139
133
 
140
134
  ## Review report shape
@@ -163,6 +157,6 @@ Write `<ISSUE>.review.md`; the script validates it:
163
157
  ## Operator override
164
158
 
165
159
  `reconcile.auto_review: false` (role.yaml) or `RECONCILE_AUTO_REVIEW=off`
166
- disables autonomous acceptance. Autonomous acceptances are fully traceable via
167
- the decision events in
168
- `_bmad-output/implementation-artifacts/bloodbank-events.jsonl`.
160
+ disables autonomous acceptance. Autonomous acceptances are traceable via the
161
+ review report, the issue evidence file, and the ticket comment the script
162
+ posts.
@@ -4,46 +4,39 @@ Status: Sentinel engine protocol (provider-agnostic)
4
4
 
5
5
  ## Purpose
6
6
 
7
- Local workflow events are the machine-readable timeline of the sentinel loop.
8
- The board remains the human command center; issue evidence files remain the
9
- close gate; this JSONL spool lets Hermes, dashboards, and future agents observe
10
- what happened.
7
+ The board remains the human command center and issue evidence files remain the
8
+ close gate. Neither depends on an event trail: `bin/issue-close-gate.sh` and
9
+ `bin/issue-autonomous-review.sh` report their verdicts on stdout/stderr and via
10
+ their exit codes, and publish nothing.
11
+
12
+ The `repo.issue.*` family the sentinel loop used to mint was retired on
13
+ 2026-08-28. It was never published to NATS and never consumed by anything, and
14
+ its shape was invalid twice over: it embedded the repo slug inside the type
15
+ (`bloodbank.v1.repo.<repo>.issue.…`) and `issue` is not in the Bloodbank §7
16
+ entity allowlist. There is no correct version of it to migrate to, so it is
17
+ gone rather than renamed.
11
18
 
12
19
  ## Emitter
13
20
 
21
+ `bin/emit-event.py` stays as a dependency-free local emitter for a family a
22
+ future pass genuinely needs:
23
+
14
24
  ```bash
15
25
  .scripts/sentinel/bin/emit-event.py <event_type> --field key=value [...]
16
26
  ```
17
27
 
18
- Appends to `_bmad-output/implementation-artifacts/bloodbank-events.jsonl`
19
- (git-ignored dev spool) using the Hermes CloudEvents envelope shape. Event types
20
- use the project repo lane `bloodbank.v1.repo.<repo>.<entity>.<action>`, where
21
- `<repo>` comes from `role.yaml`.
22
-
23
- ## Event types
24
-
25
- | Event type | When | Required data |
26
- | --- | --- | --- |
27
- | `…repo.<repo>.issue.evidence.created` | Evidence file created | `issue`, `evidence_file` |
28
- | `…repo.<repo>.issue.gate.passed` | Close gate passes | `issue`, `evidence_file` |
29
- | `…repo.<repo>.issue.gate.failed` | Close gate fails | `issue`, `evidence_file` |
30
- | `…repo.<repo>.issue.autonomous_review.decided` | An independent adversarial review clears a review-lane ticket; the loop acts on the verdict autonomously and treats `accepted` tickets as done (left in the review lane) | `issue`, `decision` (`accepted`/`held`), `drift`, `close_gate`, `reviewer_agent`, `evidence_file`, `report_file` |
31
- | `…repo.<repo>.issue.review_rollback.recorded` | A review-accepted ticket is moved back to active because a dependent proved it broken | `issue`, `surfaced_by`, `reason` |
32
- | `…repo.<repo>.issue.truthcheck.flagged` | Status/evidence mismatch found | `issue`, `reason` |
33
-
34
- ## Rules
35
-
36
- - Emit events for consequential transitions; do not invent types casually.
37
- - Event emission never replaces the board update or issue evidence.
38
- - If emission fails, continue and report the trail is incomplete.
39
- - Autonomous acceptance is legitimate only when
40
- `issue.autonomous_review.decided` is emitted with `decision=accepted` and
41
- `close_gate=pass` by `bin/issue-autonomous-review.sh`. That script will not
42
- emit an `accepted` decision while the close gate fails or drift is `significant`.
43
-
44
- ## Canonical BloodBank
45
-
46
- These project-local repo-lane events are BloodBank-*style*. Promote a type to a
47
- canonical NATS subject only after adding its JSON Schema to the BloodBank schema
48
- tree and passing validation. The local emitter does not require NATS so the loop
49
- stays reliable offline.
28
+ It appends to `_bmad-output/implementation-artifacts/bloodbank-events.jsonl`
29
+ (git-ignored dev spool) using the Hermes CloudEvents envelope shape. Nothing in
30
+ the engine calls it today.
31
+
32
+ ## Naming, before you add one
33
+
34
+ Event types are exactly four tokens — `bloodbank.<domain>.<entity>.<action>` —
35
+ with `<domain>` and `<entity>` drawn from the allowlists in
36
+ `~/code/33GOD/bloodbank/docs/event-naming.md` (§6/§7). Repo and agent identity
37
+ go in `data.repo` / `actor.agent_id`, never in a type or subject token. Schema
38
+ revision lives in `dataschema`/`schemaref`, never in the type.
39
+
40
+ Add a family when something will actually consume it register the schema in
41
+ the BloodBank schema tree first, then emit. A type nobody reads is a type that
42
+ rots into the wrong shape.
@@ -33,8 +33,8 @@ on Linear, Plane, or Trello.
33
33
  ## Pass
34
34
 
35
35
  **Trigger.** This pass runs both on the cheap heartbeat timer AND on a live
36
- **Plane board event** — a `bloodbank.evt.v1.repo.<repo>.ticket_*` event delivered
37
- by the `plane-webhook-bridge`. If a specific ticket event triggered you, FIRST
36
+ **Plane board event** — a `bloodbank.evt.repo.task.*` event (repo identity lives
37
+ in `data.repo`, never in the subject) delivered by the `plane-webhook-bridge`. If a specific ticket event triggered you, FIRST
38
38
  `tp get_issue <that ticket>`, read the change, and react to it (triage / refine /
39
39
  comment / transition per the lifecycle) before the general reconcile below.
40
40
 
@@ -68,19 +68,18 @@ comment / transition per the lifecycle) before the general reconcile below.
68
68
  independence satisfied, close gate passes): autonomously TREAT THE TICKET AS
69
69
  DONE. Leave it in the review lane as the operator's deferred-QA queue (do NOT
70
70
  auto-transition to `completed`; `--close` is an optional operator-QA-sweep
71
- flag the loop omits). Record the adversarial review report, emit the decision
72
- event, and post ONE ticket comment stating the autonomous acceptance with a
71
+ flag the loop omits). Record the adversarial review report and post ONE
72
+ ticket comment stating the autonomous acceptance with a
73
73
  pointer to the report — never a "waiting on you" comment, no grace wait. A
74
74
  dependent blocked only on this review-accepted feature is now unblocked.
75
75
  - **Held** (a real finding or gate failure): the ticket goes back to active
76
76
  (`started` if a worker takes it now, else `unstarted`); record the hold
77
- reasons and emit the decision event.
77
+ reasons in the review report and a ticket comment.
78
78
  6. **Regression rollback.** If a later dependent proves a review-accepted feature
79
79
  is ACTUALLY BROKEN, move that feature back to active (`started` if a worker
80
80
  takes it now, else `unstarted`) as a PREREQUISITE of the dependent; comment
81
- naming the dependent + symptom; emit
82
- `bloodbank.v1.repo.{{ target_repo }}.issue.review_rollback.recorded`
83
- `{issue, surfaced_by, reason}`. The dependent stays blocked on the prerequisite
81
+ naming the dependent + symptom, and record the rollback (issue, surfaced_by,
82
+ reason) in the issue evidence file. The dependent stays blocked on the prerequisite
84
83
  until it is fixed. This is expected and healthy — it is the trade for deferring
85
84
  operator QA, not a failure.
86
85
  7. **External blockers only** (credentials, third-party access, paid actions,
@@ -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}"