@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.
Files changed (57) hide show
  1. package/dist/index.js +884 -295
  2. package/dist/mcp-server.js +892 -303
  3. package/package.json +8 -2
  4. package/templates/commonproject/AGENTS.md +3 -3
  5. package/templates/commonproject/README.md +11 -12
  6. package/templates/commonproject/copier.yml +11 -31
  7. package/templates/commonproject/template/.agents/hooks/README.md +13 -26
  8. package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
  9. package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
  10. package/templates/commonproject/template/.agents/local.example.json +2 -8
  11. package/templates/commonproject/template/.agents/skills.json +6 -0
  12. package/templates/commonproject/template/.project.json.jinja +9 -13
  13. package/templates/commonproject/template/mise.toml.jinja +9 -16
  14. package/templates/hermes-agent/README.md +9 -9
  15. package/templates/hermes-agent/config.example.toml +1 -66
  16. package/templates/hermes-agent/copier.yml +4 -3
  17. package/templates/hermes-agent/docs/architecture.md +9 -12
  18. package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
  19. package/templates/hermes-agent/docs/operations.md +6 -5
  20. package/templates/hermes-agent/docs/sentinel/README.md +9 -7
  21. package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
  22. package/templates/hermes-agent/docs/sentinel/development.md +12 -13
  23. package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
  24. package/templates/hermes-agent/install-local.sh +15 -14
  25. package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
  26. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
  27. package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
  28. package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
  29. package/templates/hermes-agent/template/.gitignore.jinja +0 -2
  30. package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
  31. package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
  32. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
  33. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
  34. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
  35. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
  36. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
  37. package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
  38. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
  39. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
  40. package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
  41. package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
  42. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
  43. package/templates/hermes-agent/template/.scripts/heartbeat.sh +33 -66
  44. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
  45. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
  46. package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
  47. package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
  48. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
  49. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -33
  50. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +22 -27
  51. package/templates/hermes-agent/template/SOUL.md.jinja +49 -30
  52. package/templates/hermes-agent/template/role.yaml.jinja +35 -4
  53. package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
  54. package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
  55. package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
  56. package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
  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 or PLANE_<WORKSPACE>_API_KEY (X-API-Key header)
4
+ # Credentials: PLANE_API_KEY (X-API-Key header)
5
5
  # Endpoint: PLANE_BASE (default https://plane.delo.sh)
6
- # Board binding (repo-root .project.json `ticket_provider:`):
6
+ # Board binding (role.yaml `ticket_provider:`):
7
+ # name: plane
7
8
  # workspace: <workspace-slug> (or env PLANE_WORKSPACE)
8
- # board_id: <project-uuid> (set by create_board / 42-ticket-provider)
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 legacy role.yaml, then env.
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)"; [ -n "$PROJ" ] || PROJ="$(tp_cfg board_id)"
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 (.project.json ticket_provider.workspace or PLANE_WORKSPACE)"
124
- [ -n "$PROJ" ] || die "project not set (.project.json ticket_provider.board_id; run 42-ticket-provider.sh)"
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
  ;;
@@ -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
- - Provider issue: <ISSUE>
147
+ - Linear/Plane/Trello issue: <ISSUE>
148
148
  - Review lane reason:
149
149
  ## Reviewer
150
150
  - Reviewer agent: <independent-agent-id>
@@ -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 submodule at `./runtime/` (a separate git repo named
21
- `{{ runtime_repo_owner }}/{{ runtime_repo }}`); everything you change there is
22
- auto-checkpointed hourly + on session end.
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. The naming contract is owned
50
- by Holyfields and locked at `~/code/33GOD/bloodbank/docs/event-naming.md` —
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 manager**. You triage incoming requests from Telegram /
57
- Bloodbank command lanes, decompose them into discrete tasks on the
58
- configured ticket board, and route work to other agents in the fleet (e.g. the dev role
59
- on `bloodbank.cmd.v1.agent.{{ target_repo }}-dev.task.assign`). You do not
60
- write application code. You do not approve merges.
61
-
62
- The configured ticket provider (`{{ ticket_provider }}`) is the authoritative
63
- repo ticket board/SOT for this PM. Hermes local Kanban must NOT be an active
64
- durable task lifecycle surface. The kanban toolset is removed from the PM's cli
65
- surface. Local Kanban may only run as an explicitly configured ephemeral child
66
- execution queue and must never close/move authoritative repo tickets.
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 provider-ticket-orchestrated implementation mode
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.{{ target_repo }}.decision.recorded`
74
- - `bloodbank.v1.repo.{{ target_repo }}.intake.triaged`
75
- - `bloodbank.v1.repo.{{ target_repo }}.task.created`
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.{{ target_repo }}.code.committed`
91
- - `bloodbank.v1.repo.{{ target_repo }}.test.run.completed`
92
- - `bloodbank.v1.repo.{{ target_repo }}.pr.opened`
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.{{ target_repo }}.review.completed`
100
- - `bloodbank.v1.repo.{{ target_repo }}.pr.merged`
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
- - **Ticketing**: Always include the configured provider's ticket reference in commit messages.
126
+ - **Plane**: Always include a Plane ticket reference in commit messages.
116
127
 
117
128
  ## Memory hygiene
118
129
 
119
- Your memory is the submodule at `./runtime/memories/`. Use Hindsight for
120
- durable cross-session facts (`hindsight memory retain {{ target_repo }} "…"
121
- --context conventions`). Edit `memories/MEMORY.md` directly for the
122
- condensed mental-model summary the gateway loads on every session.
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 adapter preference (plane | trello).
25
- # Repo-owned board identity lives in repo-root .project.json, not in this role.
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.{{ target_repo }}.>"
33
- - "bloodbank.cmd.v1.agent.{{ agent_id }}.>"
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