@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
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env sh
2
2
  # Plane ticket-provider adapter.
3
3
  #
4
- # Credentials: PLANE_API_KEY or PLANE_<WORKSPACE>_API_KEY (X-API-Key header)
4
+ # Credentials: PLANE_API_KEY or PLANE_<WORKSPACE>_API_KEY (raw at runtime or
5
+ # an op:// reference resolved immediately before use)
5
6
  # Endpoint: PLANE_BASE (default https://plane.delo.sh)
6
7
  # Board binding (repo-root .project.json `ticket_provider:`):
7
8
  # workspace: <workspace-slug> (or env PLANE_WORKSPACE)
@@ -59,6 +60,20 @@ print(value, end="")
59
60
  PY
60
61
  }
61
62
 
63
+ # Resolve an approved secret reference only after selecting the exact provider
64
+ # key. The shared dotenv stays inert and the resolved value exists only in this
65
+ # provider process.
66
+ resolve_secret_value() {
67
+ value="${1:-}"
68
+ case "$value" in
69
+ op://*)
70
+ command -v op >/dev/null 2>&1 || die "1Password CLI is required for the configured Plane credential"
71
+ op read "$value" || die "failed to resolve the configured Plane credential"
72
+ ;;
73
+ *) printf '%s' "$value" ;;
74
+ esac
75
+ }
76
+
62
77
  tp_cfg() {
63
78
  [ -f "$ROLE_YAML" ] || return 0
64
79
  python3 - "$ROLE_YAML" "$1" <<'PY'
@@ -103,8 +118,9 @@ if [ -z "${PLANE_API_KEY:-}" ]; then
103
118
  if [ -z "${PLANE_API_KEY:-}" ] && [ -f "$FLEET_ENV" ]; then
104
119
  PLANE_API_KEY="$(dotenv_value "$FLEET_ENV" "$KEY")"
105
120
  fi
106
- export PLANE_API_KEY
107
121
  fi
122
+ PLANE_API_KEY="$(resolve_secret_value "${PLANE_API_KEY:-}")"
123
+ export PLANE_API_KEY
108
124
 
109
125
  # api METHOD PATH [JSON_BODY] — call Plane REST, print response body.
110
126
  api() {
@@ -144,15 +160,21 @@ pick=(named or grouped or [{}])[0]
144
160
  print(pick.get("id",""))'
145
161
  }
146
162
 
147
- # All Plane ops require the API key; fail fast and clean before any pipe.
148
- need_key
163
+ # All Plane ops except the explicit-workspace read below require the bound
164
+ # workspace API key; fail fast and clean before any pipe.
165
+ case "$OP" in describe_board) ;; *) need_key ;; esac
149
166
 
150
167
  case "$OP" in
151
168
  resolve)
152
169
  [ -n "$WS" ] || die "workspace not set (.project.json ticket_provider.workspace or PLANE_WORKSPACE)"
153
170
  [ -n "$PROJ" ] || die "project not set (.project.json ticket_provider.board_id; run 42-ticket-provider.sh)"
154
- printf '{"provider":"plane","board_id":"%s","board_url":"%s/%s/projects/%s/issues/"}\n' \
155
- "$PROJ" "$BASE" "$WS" "$PROJ"
171
+ PROJECT_DETAIL="$(api GET "projects/$PROJ/")"
172
+ LIVE_IDENTIFIER="$(printf '%s' "$PROJECT_DETAIL" | python3 -c 'import sys,json
173
+ try: print(str(json.load(sys.stdin).get("identifier") or ""))
174
+ except Exception: print("")')"
175
+ [ -n "$LIVE_IDENTIFIER" ] || die "live Plane project omitted its authoritative identifier"
176
+ printf '{"provider":"plane","board_id":"%s","board_url":"%s/%s/projects/%s/issues/","identifier":"%s"}\n' \
177
+ "$PROJ" "$BASE" "$WS" "$PROJ" "$LIVE_IDENTIFIER"
156
178
  ;;
157
179
 
158
180
  active_milestone)
@@ -236,6 +258,36 @@ print(json.dumps({"id":i.get("id",""),"key":i.get("sequence_id",""),"title":i.ge
236
258
  | python3 -c 'import sys,json; d=json.load(sys.stdin); print("ok "+str(d.get("sequence_id","")) )'
237
259
  ;;
238
260
 
261
+ describe_board)
262
+ # Read-only board lookup against an EXPLICIT workspace argument, so the
263
+ # .project.json / role.yaml / env workspace precedence can never silently
264
+ # query the wrong workspace. Emits Plane's own identifier, never a guess.
265
+ DWS="${1:?usage: describe_board <workspace> <board_id>}"
266
+ DBID="${2:?usage: describe_board <workspace> <board_id>}"
267
+ DKEYVAR="$(workspace_key "$DWS")"
268
+ DKEY="$(printenv "$DKEYVAR" 2>/dev/null || true)"
269
+ if [ -z "$DKEY" ] && [ -f "$FLEET_ENV" ]; then
270
+ DKEY="$(dotenv_value "$FLEET_ENV" "$DKEYVAR")"
271
+ fi
272
+ [ -n "$DKEY" ] || DKEY="${PLANE_API_KEY:-}"
273
+ DKEY="$(resolve_secret_value "$DKEY")"
274
+ [ -n "$DKEY" ] || die "no Plane API key for workspace '$DWS' (looked for $DKEYVAR)"
275
+ DETAIL="$(curl -fsS "$BASE/api/v1/workspaces/$DWS/projects/$DBID/" \
276
+ -H "X-API-Key: $DKEY" -H "User-Agent: curl/8.0")" \
277
+ || die "describe_board failed for $DWS/$DBID"
278
+ printf '%s' "$DETAIL" | WS="$DWS" BID="$DBID" python3 -c 'import sys, json, os
279
+ d = json.load(sys.stdin)
280
+ ident = str(d.get("identifier") or "")
281
+ if not ident:
282
+ raise SystemExit("plane: live Plane project omitted its authoritative identifier")
283
+ print(json.dumps({
284
+ "board_id": str(d.get("id") or os.environ["BID"]),
285
+ "identifier": ident,
286
+ "workspace": os.environ["WS"],
287
+ "name": str(d.get("name") or ""),
288
+ }))'
289
+ ;;
290
+
239
291
  create_board)
240
292
  NAME="${1:?usage: create_board <name> <ident> <desc>}"; IDENT="${2:-}"; DESC="${3:-}"
241
293
  [ -n "$WS" ] || die "workspace not set"
@@ -249,13 +301,23 @@ pid=next((p["id"] for p in rows if str(p.get("name","")).strip().lower()==name),
249
301
  if not pid and ident:
250
302
  pid=next((p["id"] for p in rows if (p.get("identifier") or "").upper()==ident), "")
251
303
  print(pid)')"
304
+ LIVE_IDENTIFIER=""
252
305
  if [ -n "$EXIST" ]; then PID="$EXIST"; else
253
- PID="$(api POST "projects/" \
254
- "$(python3 -c 'import json,sys; print(json.dumps({"name":sys.argv[1],"identifier":sys.argv[2],"description":sys.argv[3]}))' "$NAME" "$IDENT" "$DESC")" \
255
- | python3 -c 'import sys,json; print(json.load(sys.stdin).get("id",""))')"
306
+ CREATED="$(api POST "projects/" \
307
+ "$(python3 -c 'import json,sys; print(json.dumps({"name":sys.argv[1],"identifier":sys.argv[2],"description":sys.argv[3]}))' "$NAME" "$IDENT" "$DESC")")"
308
+ PID="$(printf '%s' "$CREATED" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("id",""))')"
309
+ LIVE_IDENTIFIER="$(printf '%s' "$CREATED" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("identifier","") or "")')"
256
310
  fi
257
311
  [ -n "$PID" ] || die "create_board failed"
258
- printf '{"board_id":"%s","board_url":"%s/%s/projects/%s/issues/"}\n' "$PID" "$BASE" "$WS" "$PID"
312
+ if [ -z "$LIVE_IDENTIFIER" ]; then
313
+ DETAIL="$(api GET "projects/$PID/")"
314
+ LIVE_IDENTIFIER="$(printf '%s' "$DETAIL" | python3 -c 'import sys,json
315
+ try: print(str(json.load(sys.stdin).get("identifier") or ""))
316
+ except Exception: print("")')"
317
+ fi
318
+ [ -n "$LIVE_IDENTIFIER" ] || die "live Plane project omitted its authoritative identifier"
319
+ printf '{"board_id":"%s","board_url":"%s/%s/projects/%s/issues/","identifier":"%s"}\n' \
320
+ "$PID" "$BASE" "$WS" "$PID" "$LIVE_IDENTIFIER"
259
321
  ;;
260
322
 
261
323
  create_issue)
@@ -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,