@delorenj/pjangler 1.2.18 → 1.2.21
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/dist/index.js +884 -295
- package/dist/mcp-server.js +892 -303
- package/package.json +8 -2
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +11 -12
- package/templates/commonproject/copier.yml +11 -31
- package/templates/commonproject/template/.agents/hooks/README.md +13 -26
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
- package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
- package/templates/commonproject/template/.agents/local.example.json +2 -8
- package/templates/commonproject/template/.agents/skills.json +6 -0
- package/templates/commonproject/template/.project.json.jinja +9 -13
- package/templates/commonproject/template/mise.toml.jinja +9 -16
- package/templates/hermes-agent/README.md +9 -9
- package/templates/hermes-agent/config.example.toml +1 -66
- package/templates/hermes-agent/copier.yml +4 -3
- package/templates/hermes-agent/docs/architecture.md +9 -12
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/operations.md +6 -5
- package/templates/hermes-agent/docs/sentinel/README.md +9 -7
- package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
- package/templates/hermes-agent/docs/sentinel/development.md +12 -13
- package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
- package/templates/hermes-agent/install-local.sh +15 -14
- package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
- package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
- package/templates/hermes-agent/template/.gitignore.jinja +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
- package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
- package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +33 -66
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -33
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +22 -27
- package/templates/hermes-agent/template/SOUL.md.jinja +49 -30
- package/templates/hermes-agent/template/role.yaml.jinja +35 -4
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
- package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
- package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
2
|
# Plane ticket-provider adapter.
|
|
3
3
|
#
|
|
4
|
-
# Credentials: PLANE_API_KEY
|
|
4
|
+
# Credentials: PLANE_API_KEY (X-API-Key header)
|
|
5
5
|
# Endpoint: PLANE_BASE (default https://plane.delo.sh)
|
|
6
|
-
# Board binding (
|
|
6
|
+
# Board binding (role.yaml `ticket_provider:`):
|
|
7
|
+
# name: plane
|
|
7
8
|
# workspace: <workspace-slug> (or env PLANE_WORKSPACE)
|
|
8
|
-
#
|
|
9
|
+
# project: <project-uuid> (set by create_board / 42-ticket-provider)
|
|
9
10
|
# state_map: { in_review: "In Review", completed: "Done" } optional
|
|
10
11
|
#
|
|
11
12
|
# Plane model: project = board, cycle = milestone, state.group in
|
|
@@ -20,21 +21,9 @@ ROLE_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
|
20
21
|
ROLE_YAML="$ROLE_DIR/role.yaml"
|
|
21
22
|
BASE="${PLANE_BASE:-https://plane.delo.sh}"
|
|
22
23
|
|
|
23
|
-
FLEET_ENV="${HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
|
|
24
|
-
if [ -f "$FLEET_ENV" ]; then
|
|
25
|
-
# shellcheck disable=SC1090
|
|
26
|
-
. "$FLEET_ENV"
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
24
|
die() { echo "plane: $*" >&2; exit 1; }
|
|
30
25
|
need_key() { [ -n "${PLANE_API_KEY:-}" ] || die "PLANE_API_KEY is not set"; }
|
|
31
26
|
|
|
32
|
-
workspace_key() {
|
|
33
|
-
key="$(printf '%s' "${1:-default}" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')"
|
|
34
|
-
[ -n "$key" ] || key="DEFAULT"
|
|
35
|
-
printf 'PLANE_%s_API_KEY' "$key"
|
|
36
|
-
}
|
|
37
|
-
|
|
38
27
|
tp_cfg() {
|
|
39
28
|
[ -f "$ROLE_YAML" ] || return 0
|
|
40
29
|
python3 - "$ROLE_YAML" "$1" <<'PY'
|
|
@@ -65,19 +54,13 @@ else:
|
|
|
65
54
|
PY
|
|
66
55
|
}
|
|
67
56
|
|
|
68
|
-
# Board binding: .project.json (SOT) first, then
|
|
57
|
+
# Board binding: .project.json (SOT) first, then role.yaml, then env.
|
|
69
58
|
WS="$(pj_cfg workspace)"; [ -n "$WS" ] || WS="$(tp_cfg workspace)"; WS="${WS:-${PLANE_WORKSPACE:-}}"
|
|
70
|
-
PROJ="$(pj_cfg board_id)"; [ -n "$PROJ" ] || PROJ="$(tp_cfg project)"
|
|
59
|
+
PROJ="$(pj_cfg board_id)"; [ -n "$PROJ" ] || PROJ="$(tp_cfg project)"
|
|
71
60
|
SM_IN_REVIEW="$(tp_cfg in_review)"; SM_IN_REVIEW="${SM_IN_REVIEW:-In Review}"
|
|
72
61
|
SM_DONE="$(tp_cfg completed)"; SM_DONE="${SM_DONE:-Done}"
|
|
73
62
|
API="$BASE/api/v1/workspaces/$WS"
|
|
74
63
|
|
|
75
|
-
if [ -z "${PLANE_API_KEY:-}" ]; then
|
|
76
|
-
KEY="$(workspace_key "$WS")"
|
|
77
|
-
eval "PLANE_API_KEY=\${$KEY:-}"
|
|
78
|
-
export PLANE_API_KEY
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
64
|
# api METHOD PATH [JSON_BODY] — call Plane REST, print response body.
|
|
82
65
|
api() {
|
|
83
66
|
need_key
|
|
@@ -85,12 +68,9 @@ api() {
|
|
|
85
68
|
if [ -n "$body" ]; then
|
|
86
69
|
curl -fsS -X "$method" "$API/$path" \
|
|
87
70
|
-H "X-API-Key: $PLANE_API_KEY" -H "Content-Type: application/json" \
|
|
88
|
-
-H "User-Agent: curl/8.0" \
|
|
89
71
|
-d "$body"
|
|
90
72
|
else
|
|
91
|
-
curl -fsS -X "$method" "$API/$path"
|
|
92
|
-
-H "X-API-Key: $PLANE_API_KEY" \
|
|
93
|
-
-H "User-Agent: curl/8.0"
|
|
73
|
+
curl -fsS -X "$method" "$API/$path" -H "X-API-Key: $PLANE_API_KEY"
|
|
94
74
|
fi
|
|
95
75
|
}
|
|
96
76
|
|
|
@@ -120,8 +100,8 @@ need_key
|
|
|
120
100
|
|
|
121
101
|
case "$OP" in
|
|
122
102
|
resolve)
|
|
123
|
-
[ -n "$WS" ] || die "workspace not set (.
|
|
124
|
-
[ -n "$PROJ" ] || die "project not set (
|
|
103
|
+
[ -n "$WS" ] || die "workspace not set (role.yaml ticket_provider.workspace or PLANE_WORKSPACE)"
|
|
104
|
+
[ -n "$PROJ" ] || die "project not set (run 42-ticket-provider.sh)"
|
|
125
105
|
printf '{"provider":"plane","board_id":"%s","board_url":"%s/%s/projects/%s/issues/"}\n' \
|
|
126
106
|
"$PROJ" "$BASE" "$WS" "$PROJ"
|
|
127
107
|
;;
|
package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md
CHANGED
|
@@ -13,7 +13,7 @@ actively tries to break the work, surface unmet acceptance criteria, hidden
|
|
|
13
13
|
regressions, and drift. On a clean adversarial verdict the loop treats the
|
|
14
14
|
ticket as done and moves on — through the ticket-provider adapter
|
|
15
15
|
(`tp transition <id> <state>`) — and emits a BloodBank decision event carrying
|
|
16
|
-
the full report. This works identically on Plane or Trello.
|
|
16
|
+
the full report. This works identically on Linear, Plane, or Trello.
|
|
17
17
|
|
|
18
18
|
The operator's verification is **deferred** to end-of-product QA over the review
|
|
19
19
|
lane, backed by a queryable decision trail. A downstream regression rollback is
|
|
@@ -144,7 +144,7 @@ Write `<ISSUE>.review.md`; the script validates it:
|
|
|
144
144
|
```markdown
|
|
145
145
|
# Autonomous Review Report: <ISSUE>
|
|
146
146
|
## Issue
|
|
147
|
-
-
|
|
147
|
+
- Linear/Plane/Trello issue: <ISSUE>
|
|
148
148
|
- Review lane reason:
|
|
149
149
|
## Reviewer
|
|
150
150
|
- Reviewer agent: <independent-agent-id>
|
package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md
CHANGED
|
@@ -19,7 +19,7 @@ approve merges.
|
|
|
19
19
|
All board access goes through the adapter (`tp`, from
|
|
20
20
|
`.scripts/lib/ticket-provider.sh`) and reasons in normalized states:
|
|
21
21
|
`backlog | unstarted | started | in_review | completed`. Never call the provider
|
|
22
|
-
directly — the engine is identical across Plane and Trello.
|
|
22
|
+
directly — the engine is identical across Linear, Plane, and Trello.
|
|
23
23
|
|
|
24
24
|
## Work-state feed
|
|
25
25
|
|
|
@@ -73,35 +73,3 @@ The loop never ends a pass with work parked waiting on the operator.
|
|
|
73
73
|
prerequisite of the dependent and record the rollback.
|
|
74
74
|
|
|
75
75
|
Board status is not proof. Repository evidence and the close gate are proof.
|
|
76
|
-
|
|
77
|
-
## Post-loop improvement (end-of-batch retro)
|
|
78
|
-
|
|
79
|
-
When the board-clearing loop reaches a resting state — no ready or actionable
|
|
80
|
-
ticket to move and no live worker to monitor (a stop condition above, not a
|
|
81
|
-
mid-flight pause) — the sentinel runs a short end-of-batch retro before the pass
|
|
82
|
-
ends. Because it fires only at rest, it never competes with live ticket flow
|
|
83
|
-
(anti-stall).
|
|
84
|
-
|
|
85
|
-
Answer three questions about the batch just cleared:
|
|
86
|
-
|
|
87
|
-
1. What hurt this batch?
|
|
88
|
-
2. What should change?
|
|
89
|
-
3. Is the fix repo-local or external/template/fleet?
|
|
90
|
-
|
|
91
|
-
ALWAYS record the reflection as a run artifact under
|
|
92
|
-
`_bmad-output/implementation-artifacts/`: the three answers, any repo-local fix
|
|
93
|
-
applied, and a clear description of each proposed external/template/fleet
|
|
94
|
-
improvement. Nothing is silently dropped — the artifact is the durable record.
|
|
95
|
-
|
|
96
|
-
Then act on the answer — do not just note it:
|
|
97
|
-
|
|
98
|
-
- Repo-local, small, safe, and within this pass's scope → implement it now as a
|
|
99
|
-
minor follow-up. If it is larger or riskier, do not start it here; treat it as
|
|
100
|
-
external and surface it below.
|
|
101
|
-
- External / template / fleet (hermes-agent template, fleet config, another repo,
|
|
102
|
-
or an undecided product/architecture decision) → the adapter cannot open issues,
|
|
103
|
-
so surface it loudly instead: post a `tp comment` on the current tracking/active
|
|
104
|
-
issue proposing the follow-up, and flag the operator (the human-facing control
|
|
105
|
-
plane) to file it. Do not attempt to create a ticket through the adapter.
|
|
106
|
-
|
|
107
|
-
The retro is evidence like any other pass output.
|
|
@@ -6,11 +6,6 @@ A cheap systemd heartbeat already decided this full pass is needed.
|
|
|
6
6
|
Working repo: the git root containing this role at `agents/hermes/{{ role }}/`.
|
|
7
7
|
Ticket provider: **{{ ticket_provider }}** (reached only through the adapter — see below).
|
|
8
8
|
|
|
9
|
-
The configured ticket provider is the authoritative repo ticket board/SOT for
|
|
10
|
-
this PM. Hermes local Kanban must NOT be an active durable task lifecycle
|
|
11
|
-
surface. Local Kanban may only run as an explicitly configured ephemeral child
|
|
12
|
-
execution queue and must never close/move authoritative repo tickets.
|
|
13
|
-
|
|
14
9
|
You are the **{{ target_repo }} PM**, running your continuous board-reconciliation
|
|
15
10
|
pass. Act autonomously, but stay inside the project contracts. Read
|
|
16
11
|
`agents/hermes/{{ role }}/SOUL.md` and the engine docs under
|
|
@@ -32,10 +27,16 @@ tp transition <id> <normalized-state> # backlog|unstarted|started|in_review|com
|
|
|
32
27
|
```
|
|
33
28
|
|
|
34
29
|
Reason in **normalized states**, not provider terms. This pass works identically
|
|
35
|
-
on Plane or Trello.
|
|
30
|
+
on Linear, Plane, or Trello.
|
|
36
31
|
|
|
37
32
|
## Pass
|
|
38
33
|
|
|
34
|
+
**Trigger.** This pass runs both on the cheap heartbeat timer AND on a live
|
|
35
|
+
**Plane board event** — a `bloodbank.evt.v1.repo.<repo>.ticket_*` event delivered
|
|
36
|
+
by the `plane-webhook-bridge`. If a specific ticket event triggered you, FIRST
|
|
37
|
+
`tp get_issue <that ticket>`, read the change, and react to it (triage / refine /
|
|
38
|
+
comment / transition per the lifecycle) before the general reconcile below.
|
|
39
|
+
|
|
39
40
|
1. Run or explicitly follow the project session-start ritual if one exists.
|
|
40
41
|
2. Reconcile: active milestone (`tp active_milestone`), issues (`tp list_issues`),
|
|
41
42
|
local evidence under `_bmad-output/implementation-artifacts/issue-evidence/`,
|
|
@@ -90,32 +91,26 @@ on Plane or Trello.
|
|
|
90
91
|
as done, move on), **held** (back to active), or a genuine **out-of-scope
|
|
91
92
|
blocker** (recorded and waited on). There is no fourth "waiting for the
|
|
92
93
|
operator's sign-off" state.
|
|
94
|
+
|
|
95
|
+
**Before going idle — always run this protocol (this is the point of the pass):**
|
|
96
|
+
|
|
97
|
+
- **(a) Unclaimed-work sweep.** If ANY ticket is claimable (`ready`/`unstarted`
|
|
98
|
+
in the active milestone, no active worker) and WIP allows, run the delegation
|
|
99
|
+
cycle (step 4) now — never go idle with claimable work sitting on the board.
|
|
100
|
+
- **(b) In-progress status sweep.** For EVERY `started` / `in_review` ticket:
|
|
101
|
+
request a status update — verify the worker is alive (its zellij session /
|
|
102
|
+
worktree / recent git activity) and post or refresh a concise status note on
|
|
103
|
+
the ticket (`tp comment`) with progress + next step. If a ticket is
|
|
104
|
+
**blocked**, actively try to UNBLOCK it: identify the blocker and act on
|
|
105
|
+
anything agent-doable (missing AC, a dependency you can drive, a stale worker
|
|
106
|
+
to re-drive); escalate ONLY a genuine external blocker (step 7). A silent or
|
|
107
|
+
stalled worker is re-driven or flagged — never left silent.
|
|
108
|
+
|
|
93
109
|
9. Update `runtime/continuous-ticket-sentinel-state.json`: `active` /
|
|
94
110
|
`blocked` / `idle` / `stalled` with the required fields (`source`, `agent_id`,
|
|
95
111
|
`repo`, `ticket_provider`, `status`, `summary`, `reason`, `updated_at`,
|
|
96
112
|
`last_activity_at`, `log_path`).
|
|
97
113
|
10. Run or follow the session-end ritual; report board status, issues touched,
|
|
98
114
|
evidence touched, and the active worker issue or blocker.
|
|
99
|
-
11. **Post-loop improvement (end-of-batch retro).** ONLY when the board-clearing
|
|
100
|
-
loop reaches a resting state this pass — no ready/actionable ticket to move and
|
|
101
|
-
no live worker to monitor (idle, or only backlog / external-blocked work
|
|
102
|
-
remains). This is a retro AT REST: it never runs while a ready ticket or a
|
|
103
|
-
healthy worker exists, and it must not block or delay live ticket flow
|
|
104
|
-
(anti-stall, step 8). Answer three questions about the batch just cleared:
|
|
105
|
-
(1) What hurt this batch? (2) What should change? (3) Is the fix repo-local or
|
|
106
|
-
external/template/fleet? ALWAYS record the reflection as a run artifact
|
|
107
|
-
(evidence) under `_bmad-output/implementation-artifacts/`: the three answers,
|
|
108
|
-
any repo-local fix applied, and a clear description of each proposed
|
|
109
|
-
external/template/fleet improvement. Nothing is silently dropped — the artifact
|
|
110
|
-
is the durable record. Then act on the answer autonomously:
|
|
111
|
-
- **Repo-local** and small, safe, and within this pass's scope: implement it now
|
|
112
|
-
as a minor follow-up inside **{{ target_repo }}**. If it is larger or riskier
|
|
113
|
-
than that, do NOT start it here — treat it as external and surface it.
|
|
114
|
-
- **External / template / fleet** (hermes-agent template, fleet config, another
|
|
115
|
-
repo, or an undecided product/architecture decision): the adapter cannot open
|
|
116
|
-
issues, so surface it loudly instead — post a `tp comment` on the current
|
|
117
|
-
tracking/active issue proposing the follow-up, and flag the operator (the
|
|
118
|
-
human-facing control plane) to file it. Do not attempt to create a ticket
|
|
119
|
-
through the adapter.
|
|
120
115
|
|
|
121
116
|
Do not rely on a hard-coded seed ticket. Query the board every pass.
|
|
@@ -17,9 +17,10 @@ You are **{{ display_name }}** — a Hermes agent provisioned to work inside the
|
|
|
17
17
|
|
|
18
18
|
You operate **only** within the working directory of `{{ target_repo }}`. You do
|
|
19
19
|
not touch files outside this repo unless the operator explicitly approves it.
|
|
20
|
-
Your HERMES_HOME is the
|
|
21
|
-
`
|
|
22
|
-
|
|
20
|
+
Your HERMES_HOME is the local runtime at `./runtime/`; Hermes loads its
|
|
21
|
+
`config.yaml` directly. Secrets, SOUL, skills, sessions, and gateway state live
|
|
22
|
+
local to that runtime (pure-local state; durable memory is the shared Hindsight
|
|
23
|
+
bank — see Memory hygiene).
|
|
23
24
|
|
|
24
25
|
## Tone
|
|
25
26
|
|
|
@@ -46,33 +47,43 @@ Envelope shape: CloudEvents 1.0, type `bloodbank.v1.<domain>.<entity>.<action>`,
|
|
|
46
47
|
`source = hermes://agent/{{ agent_id }}`. The consumer in `./runtime/` already
|
|
47
48
|
imports the envelope helper.
|
|
48
49
|
|
|
49
|
-
You **MUST NOT** invent new event `type` values.
|
|
50
|
-
|
|
50
|
+
You **MUST NOT** invent new event `type` values. Bloodbank owns the naming
|
|
51
|
+
contract at `~/code/33GOD/bloodbank/docs/event-naming.md` —
|
|
51
52
|
read it before publishing a type you haven't published before.
|
|
52
53
|
|
|
53
54
|
## Role-specific behavior
|
|
54
55
|
|
|
55
56
|
{% if role == "pm" -%}
|
|
56
|
-
You are the **project
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
You are the **project-manager ORCHESTRATOR** — the autonomous Hermes carrier of
|
|
58
|
+
Momo, and the twin of the human-drivable Momo. You share ONE board and ONE
|
|
59
|
+
Hindsight bank with it; stay attributable and never split-brain the state. You
|
|
60
|
+
triage incoming requests from Telegram / Bloodbank command lanes, decompose them
|
|
61
|
+
into discrete tasks on the Plane board, and route work to other agents (e.g. the
|
|
62
|
+
dev role on `bloodbank.cmd.v1.agent.task.assign` with
|
|
63
|
+
`data.target_agent_id = {{ target_repo }}-dev`).
|
|
64
|
+
|
|
65
|
+
**Prime directives (non-negotiable):**
|
|
66
|
+
- **Never mutate code** — every code change flows through a delegated worker.
|
|
67
|
+
- **WIP = 1**, shared with the human-drivable Momo via the driver lease
|
|
68
|
+
(`.scripts/momo-wip-lock.py` → `runtime/wip-driver.lock`) — acquire before driving,
|
|
69
|
+
back off if Momo holds it fresh; never double-drive one board. (The heartbeat
|
|
70
|
+
enforces this automatically for the reconcile pass.)
|
|
71
|
+
- **Reviewer ≠ implementer** — independent adversarial review is the normal path.
|
|
72
|
+
- **Evidence over status** — a board column is a claim; repo evidence is proof.
|
|
73
|
+
- **Anti-stall** — never park a pass on operator sign-off.
|
|
74
|
+
- You do not write application code. You do not approve merges.
|
|
67
75
|
|
|
68
76
|
Default execution workflow for implementation delivery: use
|
|
69
|
-
`subagent-driven-development` in
|
|
77
|
+
`subagent-driven-development` in kanban-orchestrated codex mode
|
|
70
78
|
(WIP=1, spec review gate, quality review gate).
|
|
71
79
|
|
|
72
80
|
Decision events you commonly emit:
|
|
73
|
-
- `bloodbank.v1.repo.
|
|
74
|
-
- `bloodbank.v1.repo.
|
|
75
|
-
- `bloodbank.v1.repo.
|
|
81
|
+
- `bloodbank.v1.repo.decision.recorded`
|
|
82
|
+
- `bloodbank.v1.repo.intake.triaged`
|
|
83
|
+
- `bloodbank.v1.repo.task.created`
|
|
84
|
+
|
|
85
|
+
Put `repo = {{ target_repo }}` in event data; never insert repo or agent
|
|
86
|
+
identifiers into Bloodbank type or subject tokens.
|
|
76
87
|
|
|
77
88
|
Template-governor command contract:
|
|
78
89
|
- If operator says `update template to capture <X>`, run `hermes-pm-template-maintenance` workflow:
|
|
@@ -87,17 +98,17 @@ write tests, run the project's `mise run test` or equivalent, open PRs, and
|
|
|
87
98
|
respond to review comments. You do not merge — that's the reviewer's call.
|
|
88
99
|
|
|
89
100
|
Events you commonly emit:
|
|
90
|
-
- `bloodbank.v1.repo.
|
|
91
|
-
- `bloodbank.v1.repo.
|
|
92
|
-
- `bloodbank.v1.repo.
|
|
101
|
+
- `bloodbank.v1.repo.code.committed`
|
|
102
|
+
- `bloodbank.v1.repo.test.completed`
|
|
103
|
+
- `bloodbank.v1.repo.pr.opened`
|
|
93
104
|
{%- elif role == "review" -%}
|
|
94
105
|
You are the **reviewer**. You read PRs critically, check against the
|
|
95
106
|
project's CONTRIBUTING and AGENTS.md, and either approve or request changes.
|
|
96
107
|
You hold the merge gate for non-trivial changes.
|
|
97
108
|
|
|
98
109
|
Events:
|
|
99
|
-
- `bloodbank.v1.repo.
|
|
100
|
-
- `bloodbank.v1.repo.
|
|
110
|
+
- `bloodbank.v1.repo.review.completed`
|
|
111
|
+
- `bloodbank.v1.repo.pr.merged`
|
|
101
112
|
{%- else -%}
|
|
102
113
|
You operate as the **{{ role }}** agent for this repo. Define your contract
|
|
103
114
|
in this file (this section), then publish a `bloodbank.v1.agent.contract.
|
|
@@ -112,11 +123,19 @@ declared` event so the fleet knows what to route to you.
|
|
|
112
123
|
- **Hostnames**: Use `*.delo.sh` for external/cross-machine access (resolved
|
|
113
124
|
via Cloudflare Tunnel), `localhost` for same-host, Docker network service
|
|
114
125
|
names for container-to-container, Tailscale for private machine-to-machine.
|
|
115
|
-
- **
|
|
126
|
+
- **Plane**: Always include a Plane ticket reference in commit messages.
|
|
116
127
|
|
|
117
128
|
## Memory hygiene
|
|
118
129
|
|
|
119
|
-
Your memory is the
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
Your durable memory is the shared **Hindsight bank `{{ target_repo }}`** — one
|
|
131
|
+
bank per PROJECT, shared with the human-drivable Momo twin. Honcho and the
|
|
132
|
+
per-agent `runtime/memories/` store are **neutralized** (see `config.yaml`
|
|
133
|
+
`memory.provider: ""`): do not rely on `MEMORY.md`/`USER.md`. Retain with
|
|
134
|
+
`hindsight memory retain {{ target_repo }} "…" --context <cat>`; recall with
|
|
135
|
+
`hindsight memory recall {{ target_repo }} "…"`.
|
|
136
|
+
|
|
137
|
+
## Doctrine
|
|
138
|
+
|
|
139
|
+
Decide on the operator's behalf using **`~/code/33GOD/momo/PILLARS.md`**
|
|
140
|
+
(canonical, priority-ordered). This soul **references** that file; it does not
|
|
141
|
+
copy it. Cite the pillar(s) that drove a consequential call in its decision event.
|
|
@@ -21,16 +21,47 @@ profile: {{ agent_id }}
|
|
|
21
21
|
telegram:
|
|
22
22
|
bot_username: "{{ bot_handle }}"
|
|
23
23
|
|
|
24
|
-
# Ticket-provider
|
|
25
|
-
#
|
|
24
|
+
# Ticket-provider binding (linear | plane | trello).
|
|
25
|
+
# The heartbeat reconciliation pass talks ONLY to this via .scripts/lib/ticket-provider.sh;
|
|
26
|
+
# swapping providers is a one-line change here. Filled in by 42-ticket-provider.sh.
|
|
26
27
|
ticket_provider:
|
|
27
28
|
name: {{ ticket_provider }}
|
|
29
|
+
board_id: ""
|
|
30
|
+
board_url: ""
|
|
31
|
+
{%- if ticket_provider == 'linear' %}
|
|
32
|
+
team: "" # Linear team key, e.g. DEL (set before first sentinel run)
|
|
33
|
+
project: "" # optional Linear project name to scope milestones/issues
|
|
34
|
+
{%- elif ticket_provider == 'plane' %}
|
|
35
|
+
workspace: "{{ plane_workspace }}"
|
|
36
|
+
project: "" # Plane project uuid (set by 42-ticket-provider.sh)
|
|
37
|
+
{%- elif ticket_provider == 'trello' %}
|
|
38
|
+
board: "" # Trello board id (set by 42-ticket-provider.sh)
|
|
39
|
+
{%- endif %}
|
|
40
|
+
# Optional normalized-state name overrides for non-standard boards.
|
|
41
|
+
in_review: "" # e.g. "In Review"
|
|
42
|
+
completed: "" # e.g. "Done"
|
|
43
|
+
|
|
44
|
+
# Board-reconciliation knobs for the heartbeat sentinel pass.
|
|
45
|
+
reconcile:
|
|
46
|
+
enabled: false # heartbeat runs the autonomous board-reconciliation pass ONLY when true; default off = checkpoint-only
|
|
47
|
+
grace_hours: 0 # 0 = no human-approval wait (adversarially-reviewed review-lane = done); set >0 to require a wait
|
|
48
|
+
auto_review: true # autonomous adversarial review (act, do not wait) on/off
|
|
49
|
+
|
|
50
|
+
# Plane project (1:1 with this agent) — retained for fleet-registry back-compat.
|
|
51
|
+
plane:
|
|
52
|
+
# Empty -> resolved from ~/.config/hermes-agent-template/config.toml [plane].workspace
|
|
53
|
+
workspace: "{{ plane_workspace }}"
|
|
54
|
+
# identifier is set by .scripts/42-ticket-provider.sh after creation (plane only)
|
|
55
|
+
identifier: ""
|
|
28
56
|
|
|
29
57
|
# Bloodbank wiring (consumed + produced subjects).
|
|
30
58
|
bloodbank:
|
|
31
59
|
subscribe:
|
|
32
|
-
- "bloodbank.evt.v1.repo
|
|
33
|
-
- "bloodbank.cmd.v1.agent
|
|
60
|
+
- "bloodbank.evt.v1.repo.>"
|
|
61
|
+
- "bloodbank.cmd.v1.agent.>"
|
|
62
|
+
routing:
|
|
63
|
+
repo: "{{ target_repo }}"
|
|
64
|
+
target_agent_id: "{{ agent_id }}"
|
|
34
65
|
producer: "hermes-agent:{{ agent_id }}"
|
|
35
66
|
|
|
36
67
|
# Runtime repo (git-tracked HERMES_HOME, auto-checkpointed).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import types
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
12
|
+
CONSUMER_PATH = ROOT / "runtime-scaffold" / "bloodbank-consumer.py"
|
|
13
|
+
GENERATED_CONSUMER_PATH = ROOT / "template" / ".runtime-scaffold" / "bloodbank-consumer.py"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_consumer():
|
|
17
|
+
sys.modules.setdefault("nats", types.ModuleType("nats"))
|
|
18
|
+
spec = importlib.util.spec_from_file_location("bloodbank_consumer_contract", CONSUMER_PATH)
|
|
19
|
+
module = importlib.util.module_from_spec(spec)
|
|
20
|
+
spec.loader.exec_module(module)
|
|
21
|
+
module.AGENT_ID = "demo-pm"
|
|
22
|
+
module.REPO = "demo"
|
|
23
|
+
module.PRODUCER = "hermes-agent:demo-pm"
|
|
24
|
+
module.SOURCE = "hermes://agent/demo-pm"
|
|
25
|
+
return module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BloodbankConsumerContractTests(unittest.TestCase):
|
|
29
|
+
@classmethod
|
|
30
|
+
def setUpClass(cls):
|
|
31
|
+
cls.temp_home = tempfile.TemporaryDirectory()
|
|
32
|
+
cls.previous_home = os.environ.get("HERMES_HOME")
|
|
33
|
+
os.environ["HERMES_HOME"] = cls.temp_home.name
|
|
34
|
+
cls.consumer = load_consumer()
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def tearDownClass(cls):
|
|
38
|
+
if cls.previous_home is None:
|
|
39
|
+
os.environ.pop("HERMES_HOME", None)
|
|
40
|
+
else:
|
|
41
|
+
os.environ["HERMES_HOME"] = cls.previous_home
|
|
42
|
+
cls.temp_home.cleanup()
|
|
43
|
+
|
|
44
|
+
def test_scaffold_copies_stay_identical(self):
|
|
45
|
+
self.assertEqual(CONSUMER_PATH.read_bytes(), GENERATED_CONSUMER_PATH.read_bytes())
|
|
46
|
+
|
|
47
|
+
def test_subscriptions_use_fixed_canonical_routes(self):
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
self.consumer.SUBJECTS,
|
|
50
|
+
["bloodbank.evt.v1.repo.>", "bloodbank.cmd.v1.agent.>"],
|
|
51
|
+
)
|
|
52
|
+
self.assertNotIn("demo", ".".join(self.consumer.SUBJECTS))
|
|
53
|
+
self.assertNotIn("demo-pm", ".".join(self.consumer.SUBJECTS))
|
|
54
|
+
|
|
55
|
+
def test_envelope_keeps_identity_out_of_type_and_subject(self):
|
|
56
|
+
envelope = self.consumer.build_envelope(
|
|
57
|
+
"bloodbank.v1.repo.issue.updated",
|
|
58
|
+
{"repo": "demo", "issue": "PJAN-1"},
|
|
59
|
+
)
|
|
60
|
+
self.assertEqual(envelope["type"], "bloodbank.v1.repo.issue.updated")
|
|
61
|
+
self.assertEqual(envelope["subject"], "bloodbank.evt.v1.repo.issue.updated")
|
|
62
|
+
self.assertEqual(envelope["data"]["repo"], "demo")
|
|
63
|
+
self.assertEqual(envelope["actor"]["agent_id"], "demo-pm")
|
|
64
|
+
self.assertEqual(envelope["source"], "hermes://agent/demo-pm")
|
|
65
|
+
self.assertNotIn("demo", envelope["type"])
|
|
66
|
+
self.assertNotIn("demo", envelope["subject"])
|
|
67
|
+
with self.assertRaisesRegex(ValueError, r"bloodbank\.v1"):
|
|
68
|
+
self.consumer.build_envelope(
|
|
69
|
+
"bloodbank.v2.repo.issue.updated",
|
|
70
|
+
{"repo": "demo"},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def test_repo_events_route_by_data_repo(self):
|
|
74
|
+
subject = "bloodbank.evt.v1.repo.issue.updated"
|
|
75
|
+
envelope = self.consumer.build_envelope(
|
|
76
|
+
"bloodbank.v1.repo.issue.updated",
|
|
77
|
+
{"repo": "demo"},
|
|
78
|
+
)
|
|
79
|
+
self.assertTrue(self.consumer._is_for_consumer(subject, envelope))
|
|
80
|
+
envelope["data"]["repo"] = "another-repo"
|
|
81
|
+
self.assertFalse(self.consumer._is_for_consumer(subject, envelope))
|
|
82
|
+
|
|
83
|
+
def test_agent_commands_route_by_target_agent_id(self):
|
|
84
|
+
subject = "bloodbank.cmd.v1.agent.task.assign"
|
|
85
|
+
envelope = self.consumer.build_envelope(
|
|
86
|
+
"bloodbank.v1.agent.task.assign",
|
|
87
|
+
{"target_agent_id": "demo-pm"},
|
|
88
|
+
kind="command",
|
|
89
|
+
)
|
|
90
|
+
self.assertTrue(self.consumer._is_for_consumer(subject, envelope))
|
|
91
|
+
envelope["data"]["target_agent_id"] = "other-agent"
|
|
92
|
+
self.assertFalse(self.consumer._is_for_consumer(subject, envelope))
|
|
93
|
+
|
|
94
|
+
def test_rejects_identifier_bearing_or_mismatched_routes(self):
|
|
95
|
+
envelope = self.consumer.build_envelope(
|
|
96
|
+
"bloodbank.v1.repo.issue.updated",
|
|
97
|
+
{"repo": "demo"},
|
|
98
|
+
)
|
|
99
|
+
self.assertFalse(
|
|
100
|
+
self.consumer._is_for_consumer(
|
|
101
|
+
"bloodbank.evt.v1.repo.demo.issue.updated",
|
|
102
|
+
envelope,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
self.assertFalse(
|
|
106
|
+
self.consumer._is_for_consumer(
|
|
107
|
+
"bloodbank.evt.v1.repo.issue.created",
|
|
108
|
+
envelope,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
envelope["kind"] = "command"
|
|
112
|
+
self.assertFalse(
|
|
113
|
+
self.consumer._is_for_consumer(
|
|
114
|
+
"bloodbank.evt.v1.repo.issue.updated",
|
|
115
|
+
envelope,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def test_generated_contract_docs_do_not_put_identifiers_in_routes(self):
|
|
120
|
+
coupled_paths = [
|
|
121
|
+
ROOT / "docs" / "architecture.md",
|
|
122
|
+
ROOT / "docs" / "operations.md",
|
|
123
|
+
ROOT / "template" / "SOUL.md.jinja",
|
|
124
|
+
ROOT / "template" / "role.yaml.jinja",
|
|
125
|
+
ROOT / "runtime-scaffold" / "memories" / "MEMORY.md",
|
|
126
|
+
ROOT / "template" / ".runtime-scaffold" / "memories" / "MEMORY.md",
|
|
127
|
+
]
|
|
128
|
+
forbidden = re.compile(
|
|
129
|
+
r"bloodbank\.(?:v1\.repo|evt\.v1\.repo|cmd\.v1\.agent)\."
|
|
130
|
+
r"(?:\{\{|<repo>|<agent_id>)"
|
|
131
|
+
)
|
|
132
|
+
for path in coupled_paths:
|
|
133
|
+
with self.subTest(path=path):
|
|
134
|
+
self.assertIsNone(forbidden.search(path.read_text()))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
unittest.main()
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Fan the project's enabled skill set out to every agent CLI that does NOT read
|
|
3
|
-
# .agents/ natively, as per-skill symlinks.
|
|
4
|
-
#
|
|
5
|
-
# SSOT: .agents/skills/ (committed inherited skills + on-enter ./skills/* links)
|
|
6
|
-
# Targets: a table of per-CLI skill dirs, each with a SCOPE:
|
|
7
|
-
# global -> a shared per-user dir (e.g. ~/.codex/skills): link ours in, never
|
|
8
|
-
# clobber foreign entries, removed on leave (unlink-*-from-clis.sh).
|
|
9
|
-
# local -> a project-scoped mirror dir (e.g. ./.kimi-code/skills) that is fully
|
|
10
|
-
# ours: stale real copies / foreign symlinks are replaced, and
|
|
11
|
-
# disabled/deferred skills are pruned. Persists across leave.
|
|
12
|
-
#
|
|
13
|
-
# Per-dev controls — .agents/local.json (see .agents/local.example.json):
|
|
14
|
-
# skills.disabled[] never link these
|
|
15
|
-
# skills.defer_to_global if true, skip any skill that ALSO exists in your global
|
|
16
|
-
# SSOT (~/.agents/skills) so your global copy wins and you
|
|
17
|
-
# get zero duplicates. Teammates omit it → inherit all.
|
|
18
|
-
#
|
|
19
|
-
# Add a CLI: append one "dir|scope" line to SKILL_TARGETS. That's the whole change.
|
|
20
|
-
set -euo pipefail
|
|
21
|
-
|
|
22
|
-
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
-
project_root="$(cd "${script_dir}/../.." && pwd)"
|
|
24
|
-
agents_skills="${project_root}/.agents/skills"
|
|
25
|
-
|
|
26
|
-
SKILL_TARGETS=(
|
|
27
|
-
"${CODEX_HOME:-$HOME/.codex}/skills|global"
|
|
28
|
-
"${project_root}/.kimi-code/skills|local"
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
local_config="${project_root}/.agents/hooks/lib/local-config.sh"
|
|
32
|
-
if [ -f "$local_config" ]; then
|
|
33
|
-
# shellcheck source=/dev/null
|
|
34
|
-
source "$local_config"
|
|
35
|
-
else
|
|
36
|
-
skill_disabled() { return 1; }
|
|
37
|
-
skills_defer_to_global() { return 1; }
|
|
38
|
-
skill_is_global() { return 1; }
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
if [ ! -d "$agents_skills" ]; then
|
|
42
|
-
echo "No .agents/skills/ found. Nothing to fan out."
|
|
43
|
-
exit 0
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
defer=0
|
|
47
|
-
skills_defer_to_global && defer=1
|
|
48
|
-
|
|
49
|
-
# Should this skill be skipped (disabled, or deferred to the dev's global system)?
|
|
50
|
-
is_skipped() {
|
|
51
|
-
local name="$1"
|
|
52
|
-
skill_disabled "$name" && return 0
|
|
53
|
-
[ "$defer" = "1" ] && skill_is_global "$name" && return 0
|
|
54
|
-
return 1
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
# readlink with any trailing slash stripped (older links were stored with one).
|
|
58
|
-
_readlink_norm() {
|
|
59
|
-
local l; l="$(readlink "$1" 2>/dev/null || echo "")"
|
|
60
|
-
printf '%s' "${l%/}"
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
link_into() {
|
|
64
|
-
local dir="$1" scope="$2" linked=0 skipped=0 pruned=0
|
|
65
|
-
mkdir -p "$dir"
|
|
66
|
-
for skill_path in "$agents_skills"/*; do
|
|
67
|
-
[ -d "$skill_path" ] || continue
|
|
68
|
-
local name target
|
|
69
|
-
name="$(basename "$skill_path")"
|
|
70
|
-
[[ "$name" == ".system" ]] && continue
|
|
71
|
-
target="${dir}/${name}"
|
|
72
|
-
|
|
73
|
-
if is_skipped "$name"; then
|
|
74
|
-
# Yield the slot: remove a link WE previously made (so the dev's global copy
|
|
75
|
-
# can claim it), and prune stale entries from our fully-managed mirror.
|
|
76
|
-
if [ -L "$target" ] && [ "$(_readlink_norm "$target")" = "$skill_path" ]; then
|
|
77
|
-
rm -f "$target"
|
|
78
|
-
pruned=$((pruned + 1))
|
|
79
|
-
elif [ "$scope" = "local" ] && { [ -L "$target" ] || [ -e "$target" ]; }; then
|
|
80
|
-
rm -rf "$target"
|
|
81
|
-
pruned=$((pruned + 1))
|
|
82
|
-
fi
|
|
83
|
-
skipped=$((skipped + 1))
|
|
84
|
-
continue
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
if [ -L "$target" ] && [ "$(_readlink_norm "$target")" = "$skill_path" ]; then
|
|
88
|
-
linked=$((linked + 1))
|
|
89
|
-
continue
|
|
90
|
-
fi
|
|
91
|
-
|
|
92
|
-
if [ -e "$target" ] || [ -L "$target" ]; then
|
|
93
|
-
if [ "$scope" = "local" ]; then
|
|
94
|
-
rm -rf "$target" # our mirror: replace stale copy / foreign link
|
|
95
|
-
else
|
|
96
|
-
skipped=$((skipped + 1)) # shared dir: leave foreign entries untouched
|
|
97
|
-
continue
|
|
98
|
-
fi
|
|
99
|
-
fi
|
|
100
|
-
|
|
101
|
-
ln -s "$skill_path" "$target"
|
|
102
|
-
linked=$((linked + 1))
|
|
103
|
-
done
|
|
104
|
-
echo " ${dir} (${scope}): ${linked} linked, ${skipped} skipped, ${pruned} pruned"
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
echo "Fanning .agents/skills -> agent CLIs (defer_to_global=${defer})"
|
|
108
|
-
for entry in "${SKILL_TARGETS[@]}"; do
|
|
109
|
-
link_into "${entry%%|*}" "${entry##*|}"
|
|
110
|
-
done
|