@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.
- package/.mise/scripts/link-agentfiles.sh +38 -5
- package/README.md +20 -0
- package/dist/assets/project-notebook-skill/SHA256SUMS +10 -0
- package/dist/assets/project-notebook-skill/SKILL.md +64 -0
- package/dist/assets/project-notebook-skill/agents/openai.yaml +6 -0
- package/dist/assets/project-notebook-skill/export-manifest.json +56 -0
- package/dist/assets/project-notebook-skill/hooks/claude.settings.json +26 -0
- package/dist/assets/project-notebook-skill/hooks/hooks.master.json +26 -0
- package/dist/assets/project-notebook-skill/hooks/session-end.sh +228 -0
- package/dist/assets/project-notebook-skill/hooks/session-start.sh +228 -0
- package/dist/assets/project-notebook-skill/references/configuration.md +93 -0
- package/dist/assets/project-notebook-skill/references/recovery.md +54 -0
- package/dist/assets/project-notebook-skill/scripts/project-hooks.py +865 -0
- package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +848 -0
- package/dist/index.js +16982 -8532
- package/dist/index.js.map +7 -0
- package/dist/mcp-server.js +14107 -8089
- package/dist/mcp-server.js.map +7 -0
- package/dist/prompt.js +2 -1
- package/dist/prompt.js.map +7 -0
- package/package.json +9 -4
- package/templates/commonproject/copier.yml +14 -5
- package/templates/commonproject/template/.mise/scripts/link-agentfiles.sh +38 -5
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +60 -2
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +479 -24
- package/templates/commonproject/template/mise.toml.jinja +12 -6
- package/templates/hermes-agent/copier.yml +16 -3
- package/templates/hermes-agent/template/.gitignore.jinja +1 -0
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +7 -4
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +88 -94
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +44 -21
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +182 -171
- package/templates/hermes-agent/template/.scripts/31-slack.sh +260 -165
- package/templates/hermes-agent/template/.scripts/40-plane.sh +45 -36
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +210 -41
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +140 -22
- package/templates/hermes-agent/template/.scripts/80-registry.sh +42 -6
- package/templates/hermes-agent/template/.scripts/99-summary.sh +69 -16
- package/templates/hermes-agent/template/.scripts/_lib.sh +827 -10
- package/templates/hermes-agent/template/.scripts/channel-transaction.py +2340 -0
- package/templates/hermes-agent/template/.scripts/config.example.toml +8 -2
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +5 -1
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -3
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +29 -2
- package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +182 -0
- package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +108 -0
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +9 -0
- package/templates/hermes-agent/template/.scripts/lib/voice-config.py +546 -0
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +24 -1
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +72 -10
- package/templates/hermes-agent/template/.scripts/providers/trello.sh +25 -2
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +5 -16
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +2 -16
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +13 -19
- package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +29 -36
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -8
- package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +260 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +14 -16
- package/templates/hermes-agent/template/hermes.jinja +1 -1
- 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 (
|
|
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
|
|
148
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
|
6
|
-
# drift attestation
|
|
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
|
-
|
|
49
|
-
#
|
|
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;
|
|
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).
|
|
4
|
-
#
|
|
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"
|
package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md
CHANGED
|
@@ -112,29 +112,23 @@ loop:
|
|
|
112
112
|
.scripts/sentinel/bin/issue-autonomous-review.sh <ISSUE> <REPORT> --close
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
## Decision
|
|
115
|
+
## Decision record
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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>`)
|
|
130
|
-
the dependent + symptom
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
167
|
-
the
|
|
168
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
19
|
-
(git-ignored dev spool) using the Hermes CloudEvents envelope shape.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
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
|
|
72
|
-
|
|
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
|
|
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
|
|
82
|
-
|
|
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,
|