@delorenj/pjangler 1.3.0 → 1.3.7

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 (26) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +4510 -2485
  3. package/dist/mcp-server.js +4166 -1959
  4. package/dist/prompt.js +404 -0
  5. package/package.json +7 -5
  6. package/templates/commonproject/copier.yml +6 -1
  7. package/templates/commonproject/template/.mise/scripts/provision-packs.py +14 -50
  8. package/templates/hermes-agent/copier.yml +8 -11
  9. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
  10. package/templates/hermes-agent/template/.scripts/01-config.sh +9 -0
  11. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
  12. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +68 -4
  13. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
  14. package/templates/hermes-agent/template/.scripts/70-systemd.sh +73 -43
  15. package/templates/hermes-agent/template/.scripts/80-registry.sh +6 -0
  16. package/templates/hermes-agent/template/.scripts/_lib.sh +62 -6
  17. package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
  18. package/templates/hermes-agent/template/.scripts/heartbeat.sh +13 -1
  19. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
  20. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +734 -0
  21. package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
  22. package/templates/hermes-agent/template/.scripts/providers/plane.sh +1 -1
  23. package/templates/hermes-agent/template/SOUL.md.jinja +44 -8
  24. package/templates/hermes-agent/template/hermes.jinja +20 -8
  25. package/templates/hermes-agent/template/momo.jinja +177 -0
  26. package/templates/hermes-agent/template/role.yaml.jinja +19 -19
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # Lifecycle helper — resolves the CANONICAL Krebs ticket lifecycle for this role.
3
+ #
4
+ # The state machine is NOT defined here. It lives in
5
+ # krebs/spec/lifecycle.v1.yaml, which is the contract between ticket providers
6
+ # and fleet orchestrators. This script is a reader: it answers "which tp band
7
+ # does phase X map to" and "is this ticket stale" without any repo growing its
8
+ # own private copy of the phase list. A second copy is how a PM ends up
9
+ # transitioning against labels the board no longer uses.
10
+ #
11
+ # Provider LABELS are deliberately absent — those belong to the tp adapter's
12
+ # normalized-state map (.scripts/providers/<provider>.sh). This layer speaks
13
+ # only phases and the five tp bands.
14
+ #
15
+ # Usage:
16
+ # lifecycle.sh phases list every phase with band + staleness
17
+ # lifecycle.sh band <phase> print the tp band for a phase
18
+ # lifecycle.sh staleness <phase> print staleness minutes ("-" if none)
19
+ # lifecycle.sh is-terminal <phase> exit 0 when the phase is terminal
20
+ # lifecycle.sh stale <phase> <iso8601> exit 0 when that timestamp is stale
21
+ # lifecycle.sh spec print the resolved spec path
22
+ set -euo pipefail
23
+
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+
26
+ # Resolution order: explicit override, the 33GOD checkout, then a sibling walk
27
+ # for hosts that keep the platform elsewhere.
28
+ resolve_spec() {
29
+ if [ -n "${KREBS_LIFECYCLE_SPEC:-}" ] && [ -r "$KREBS_LIFECYCLE_SPEC" ]; then
30
+ printf '%s\n' "$KREBS_LIFECYCLE_SPEC"; return 0
31
+ fi
32
+ local candidates=(
33
+ "$HOME/code/33GOD/krebs/spec/lifecycle.v1.yaml"
34
+ "$SCRIPT_DIR/../../../../krebs/spec/lifecycle.v1.yaml"
35
+ "$SCRIPT_DIR/../../../../../krebs/spec/lifecycle.v1.yaml"
36
+ )
37
+ local c
38
+ for c in "${candidates[@]}"; do
39
+ [ -r "$c" ] && { printf '%s\n' "$c"; return 0; }
40
+ done
41
+ return 1
42
+ }
43
+
44
+ SPEC="$(resolve_spec || true)"
45
+
46
+ need_spec() {
47
+ if [ -z "$SPEC" ]; then
48
+ echo "lifecycle: canonical spec not found (set KREBS_LIFECYCLE_SPEC)" >&2
49
+ echo "lifecycle: expected krebs/spec/lifecycle.v1.yaml in the 33GOD checkout" >&2
50
+ exit 2
51
+ fi
52
+ if ! command -v python3 >/dev/null 2>&1; then
53
+ echo "lifecycle: python3 required to read $SPEC" >&2
54
+ exit 2
55
+ fi
56
+ }
57
+
58
+ # One python entrypoint for every query keeps YAML parsing in exactly one place.
59
+ query() {
60
+ need_spec
61
+ python3 - "$SPEC" "$@" <<'PY'
62
+ import sys
63
+ try:
64
+ import yaml
65
+ except ImportError:
66
+ sys.exit("lifecycle: PyYAML required to read the lifecycle spec")
67
+
68
+ spec_path, op = sys.argv[1], sys.argv[2]
69
+ arg = sys.argv[3] if len(sys.argv) > 3 else None
70
+ arg2 = sys.argv[4] if len(sys.argv) > 4 else None
71
+
72
+ spec = yaml.safe_load(open(spec_path)) or {}
73
+ states = {s["phase"]: s for s in (spec.get("states") or []) if s.get("phase")}
74
+ if not states:
75
+ sys.exit(f"lifecycle: no states in {spec_path}")
76
+
77
+ def need_phase(p):
78
+ if p not in states:
79
+ sys.exit(f"lifecycle: unknown phase {p!r}; known: {', '.join(states)}")
80
+ return states[p]
81
+
82
+ if op == "phases":
83
+ print(f"{'phase':<14} {'tp_band':<12} {'terminal':<9} stale_after")
84
+ for name, s in states.items():
85
+ stale = s.get("staleness_minutes")
86
+ print(f"{name:<14} {str(s.get('tp_band')):<12} "
87
+ f"{str(bool(s.get('terminal'))).lower():<9} "
88
+ f"{(str(stale) + 'm') if stale else '-'}")
89
+ elif op == "band":
90
+ print(need_phase(arg)["tp_band"])
91
+ elif op == "staleness":
92
+ print(need_phase(arg).get("staleness_minutes") or "-")
93
+ elif op == "is-terminal":
94
+ sys.exit(0 if need_phase(arg).get("terminal") else 1)
95
+ elif op == "stale":
96
+ from datetime import datetime, timezone
97
+ mins = need_phase(arg).get("staleness_minutes")
98
+ if not mins:
99
+ sys.exit(1) # phase has no staleness budget -> never stale
100
+ try:
101
+ ts = datetime.fromisoformat(arg2.replace("Z", "+00:00"))
102
+ except Exception:
103
+ sys.exit(f"lifecycle: unparseable timestamp {arg2!r} (want ISO 8601)")
104
+ if ts.tzinfo is None:
105
+ ts = ts.replace(tzinfo=timezone.utc)
106
+ age = (datetime.now(timezone.utc) - ts).total_seconds() / 60
107
+ sys.exit(0 if age > mins else 1)
108
+ else:
109
+ sys.exit(f"lifecycle: unknown operation {op!r}")
110
+ PY
111
+ }
112
+
113
+ case "${1:-phases}" in
114
+ phases) query phases ;;
115
+ band) query band "${2:?phase required}" ;;
116
+ staleness) query staleness "${2:?phase required}" ;;
117
+ is-terminal) query is-terminal "${2:?phase required}" ;;
118
+ stale) query stale "${2:?phase required}" "${3:?iso8601 timestamp required}" ;;
119
+ spec) need_spec; printf '%s\n' "$SPEC" ;;
120
+ --help|-h|help)
121
+ sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' ;;
122
+ *)
123
+ echo "lifecycle: unknown operation ${1}" >&2
124
+ echo "try: phases | band <phase> | staleness <phase> | is-terminal <phase> | stale <phase> <ts> | spec" >&2
125
+ exit 2 ;;
126
+ esac
@@ -99,7 +99,7 @@ API="$BASE/api/v1/workspaces/$WS"
99
99
 
100
100
  if [ -z "${PLANE_API_KEY:-}" ]; then
101
101
  KEY="$(workspace_key "$WS")"
102
- eval "PLANE_API_KEY=\${$KEY:-}"
102
+ PLANE_API_KEY="$(printenv "$KEY" 2>/dev/null || true)"
103
103
  if [ -z "${PLANE_API_KEY:-}" ] && [ -f "$FLEET_ENV" ]; then
104
104
  PLANE_API_KEY="$(dotenv_value "$FLEET_ENV" "$KEY")"
105
105
  fi
@@ -147,14 +147,50 @@ declared` event so the fleet knows what to route to you.
147
147
  via Cloudflare Tunnel), `localhost` for same-host, Docker network service
148
148
  names for container-to-container, Tailscale for private machine-to-machine.
149
149
 
150
- ## Memory hygiene
151
-
152
- Your durable memory is the shared **Hindsight bank `{{ target_repo }}`** — one
153
- bank per PROJECT, shared with the human-drivable Momo twin. Honcho and the
154
- per-agent `runtime/memories/` store are **neutralized** (see `config.yaml`
155
- `memory.provider: ""`): do not rely on `MEMORY.md`/`USER.md`. Retain with
156
- `hindsight memory retain {{ target_repo }} "…" --context <cat>`; recall with
157
- `hindsight memory recall {{ target_repo }} "…"`.
150
+ ## Memory: two namespaces, two questions
151
+
152
+ You have **two** memory stores. They do not compete they answer opposite
153
+ questions, and you are expected to use both and play them off each other.
154
+
155
+ | | **Identity memory** | **Project memory** |
156
+ | --- | --- | --- |
157
+ | Bank | `agent-{{ agent_id }}` | `{{ target_repo }}` |
158
+ | Anchored to | **who you are** | **which repo** |
159
+ | Follows you across repos | yes | no |
160
+ | Written by | the runtime, automatically | you, explicitly |
161
+ | Read by | you alone | every agent on this repo |
162
+ | Answers | "which projects have I worked on, and how do I work?" | "what is true about this repo, and which agent learned it?" |
163
+
164
+ **Identity memory** is wired to the Hermes memory provider
165
+ (`memory.bank_id_template: agent-{profile}`), so it accrues on its own from
166
+ your turns. It is keyed to your profile name, **never** to a repo or working
167
+ directory — change directories, change projects, it follows you. Treat it as
168
+ self-referential: your capabilities, your recurring mistakes and the
169
+ corrections that stuck, operator preferences you have learned, and the shape of
170
+ the projects you have touched. Do not put repo facts here; they would be
171
+ invisible to every other agent working that repo.
172
+
173
+ **Project memory** is the shared, temporally-sequenced record of a repository,
174
+ queried by many agents including the human-drivable Momo twin. Write it
175
+ explicitly, and always carry provenance — name yourself in the content so a
176
+ later reader can answer *which agent experienced this*:
177
+
178
+ ```bash
179
+ hindsight memory retain {{ target_repo }} "{{ agent_id }}: <fact>" --context <cat>
180
+ hindsight memory recall {{ target_repo }} "<question>"
181
+ ```
182
+
183
+ **The synergy.** Before starting work in a repo you have not touched lately,
184
+ recall from BOTH: project memory tells you the state of the code; identity
185
+ memory tells you how *you* previously failed or succeeded here and what the
186
+ operator asked you to do differently. When you learn something, route it by
187
+ asking one question — *would another agent on this repo need this?* If yes it
188
+ is project memory; if it is only true of you, it is identity memory. A fact
189
+ about the operator's preferences is identity memory; a fact about the build is
190
+ project memory.
191
+
192
+ `MEMORY.md` / `USER.md` are live again and are fed by the provider — they are a
193
+ projection of identity memory, not a separate store to hand-maintain.
158
194
 
159
195
  ## Doctrine
160
196
 
@@ -7,6 +7,17 @@ set -euo pipefail
7
7
  ROLE_DIR="$(cd "$(dirname "$0")" && pwd)"
8
8
  RUNTIME_HOME="$ROLE_DIR/runtime"
9
9
 
10
+ FLEET_ENV="${HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
11
+ FLEET_ENV_LIBRARY="$ROLE_DIR/.scripts/lib/fleet-env.sh"
12
+ FLEET_ENV_PARSER="$ROLE_DIR/.scripts/lib/parse-fleet-env.py"
13
+ if [[ ! -f "$FLEET_ENV_LIBRARY" || -L "$FLEET_ENV_LIBRARY" ]]; then
14
+ echo "hermes: trusted fleet environment loader unavailable" >&2
15
+ exit 1
16
+ fi
17
+ # shellcheck source=.scripts/lib/fleet-env.sh
18
+ builtin source "$FLEET_ENV_LIBRARY"
19
+ load_fleet_environment "$FLEET_ENV" "$FLEET_ENV_PARSER"
20
+
10
21
  FLEET_HOME="${HERMES_FLEET_HOME:-$HOME/.hermes}"
11
22
  PROFILE_NAME="${HERMES_PROFILE_NAME:-{{ agent_id }}}"
12
23
 
@@ -16,19 +27,20 @@ PROFILE_NAME="${HERMES_PROFILE_NAME:-{{ agent_id }}}"
16
27
  # of a "profiles" dir makes get_active_profile_name() report "default" and
17
28
  # _global_auth_file_path() return None — silently disabling shared fleet auth
18
29
  # and giving the agent a divergent config.yaml. The profile dir is a REAL dir
19
- # whose shared entries (config.yaml, .env, skills) symlink to the fleet root
20
- # and whose owned entries (memories, sessions, state.db, ...) symlink into
21
- # $RUNTIME_HOME. Provision it with: pj migrate hermes.runtime-singleton
30
+ # whose shared entries (.env, skills) symlink to the fleet root and whose owned
31
+ # entries (memories, sessions, state.db, ...) symlink into $RUNTIME_HOME.
32
+ #
33
+ # config.yaml is NOT symlinked — it is GENERATED as
34
+ # deep_merge(fleet config.yaml, <profile>/config.delta.yaml). Symlinking it
35
+ # detached the profile on the first in-agent write (Hermes' atomic write does
36
+ # os.replace, which swaps the symlink for a regular file) and left no way to
37
+ # override anything. Edit config.delta.yaml, then render.
38
+ # Provision it with: pj migrate hermes.runtime-singleton
22
39
  HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
23
40
  if ! REPO_ROOT="$(git -C "$ROLE_DIR" rev-parse --show-toplevel 2>/dev/null)"; then
24
41
  REPO_ROOT="$ROLE_DIR"
25
42
  fi
26
43
 
27
- FLEET_ENV="${HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
28
- if [[ -f "$FLEET_ENV" ]]; then
29
- # shellcheck disable=SC1090
30
- source "$FLEET_ENV"
31
- fi
32
44
  # Never pass an identity-bearing chat credential from shared fleet state into
33
45
  # a profile process. Hermes loads the profile's own runtime/.env itself.
34
46
  unset TELEGRAM_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env bash
2
+ # Momo provider dispatcher for {{ target_repo }} ({{ role }}).
3
+ #
4
+ # The stable, per-repo entrypoint Momo (and `pj audit --profile
5
+ # momo-lifecycle-plane`) uses to reach this project's ticket board. It is a thin
6
+ # seam ON PURPOSE: all real provider logic already lives in
7
+ # .scripts/lib/ticket-provider.sh, which dispatches to
8
+ # .scripts/providers/<provider>.sh based on repo-root .project.json. This file
9
+ # adds no second implementation of that — duplicating it is how a repo ends up
10
+ # with two disagreeing notions of "what board am I bound to".
11
+ #
12
+ # What this adds over calling `tp` directly:
13
+ # * one discoverable path per role (agents/hermes/<role>/momo) instead of
14
+ # "source the right lib from the right cwd"
15
+ # * --help / --smoke, so readiness is checkable without credentials
16
+ # * the canonical Krebs phase -> tp-band map, so callers reason in lifecycle
17
+ # phases rather than hardcoding provider labels
18
+ #
19
+ # Operations are forwarded verbatim to `tp`. See the contract at the top of
20
+ # .scripts/lib/ticket-provider.sh: resolve, active_milestone, list_issues,
21
+ # get_issue, comment, transition, create_board, create_issue.
22
+ set -euo pipefail
23
+
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ LIB="$SCRIPT_DIR/.scripts/lib/ticket-provider.sh"
26
+
27
+ # Canonical lifecycle spec. Phases are Krebs' vocabulary; tp bands are the five
28
+ # normalized states every provider adapter must map. Kept in sync with
29
+ # krebs/spec/lifecycle.v1.yaml — that file is the source of truth.
30
+ LIFECYCLE_SPEC="${KREBS_LIFECYCLE_SPEC:-$HOME/code/33GOD/krebs/spec/lifecycle.v1.yaml}"
31
+
32
+ usage() {
33
+ cat <<'MOMO_HELP'
34
+ momo — ticket-board dispatcher for this repo's Hermes role
35
+
36
+ USAGE
37
+ momo <operation> [args...] forward an operation to the bound ticket provider
38
+ momo --help show this help
39
+ momo --smoke [root|nested] verify wiring without credentials
40
+ momo --lifecycle print the canonical phase -> tp-band map
41
+ momo --provider print the resolved provider name
42
+
43
+ OPERATIONS (implemented by .scripts/providers/<provider>.sh)
44
+ resolve -> {provider, board_id, board_url}
45
+ active_milestone -> {id, name, state}
46
+ list_issues -> [ {id,key,title,state,state_type,...} ]
47
+ get_issue <id> -> {id,key,title,description,acceptance,...}
48
+ comment <id> <body> -> comment id
49
+ transition <id> <state> -> backlog|unstarted|started|in_review|completed|cancelled
50
+ create_board <name> <id> <d> -> {board_id, board_url}
51
+ create_issue [--if-absent] <title> [desc]
52
+
53
+ The provider and board binding come from repo-root .project.json
54
+ (ticket_provider.*). Credentials come from the environment; see the header of
55
+ the matching .scripts/providers/<provider>.sh.
56
+ MOMO_HELP
57
+ }
58
+
59
+ # Phase -> tp band, read from the canonical spec when present so this never
60
+ # becomes a stale second copy. The inline fallback exists only so --lifecycle
61
+ # still answers on a host without the 33GOD checkout.
62
+ print_lifecycle() {
63
+ if [ -r "$LIFECYCLE_SPEC" ] && command -v python3 >/dev/null 2>&1; then
64
+ python3 - "$LIFECYCLE_SPEC" <<'PY' && return 0
65
+ import sys
66
+ try:
67
+ import yaml
68
+ except ImportError:
69
+ sys.exit(1)
70
+ try:
71
+ spec = yaml.safe_load(open(sys.argv[1]))
72
+ except Exception:
73
+ sys.exit(1)
74
+ states = spec.get("states") or []
75
+ if not states:
76
+ sys.exit(1)
77
+ print(f"{'phase':<14} {'tp_band':<12} {'terminal':<9} stale_after")
78
+ for s in states:
79
+ stale = s.get("staleness_minutes")
80
+ print(f"{str(s.get('phase')):<14} {str(s.get('tp_band')):<12} "
81
+ f"{str(bool(s.get('terminal'))).lower():<9} "
82
+ f"{(str(stale) + 'm') if stale else '-'}")
83
+ PY
84
+ fi
85
+ # Fallback: spec unreadable or PyYAML absent.
86
+ cat <<'FALLBACK'
87
+ phase tp_band terminal stale_after
88
+ backlog backlog false -
89
+ triage unstarted false 10m
90
+ refining unstarted false 30m
91
+ ready unstarted false -
92
+ in_progress started false 120m
93
+ review in_review false 15m
94
+ qa in_review false -
95
+ FALLBACK
96
+ echo "(fallback map: could not read $LIFECYCLE_SPEC)" >&2
97
+ }
98
+
99
+ require_lib() {
100
+ if [ ! -r "$LIB" ]; then
101
+ echo "momo: ticket-provider lib missing: $LIB" >&2
102
+ echo "momo: run the role's provisioning scripts, or pj migrate hermes.pm-scaffold" >&2
103
+ return 2
104
+ fi
105
+ }
106
+
107
+ # Credential-free wiring check. Verifies the pieces a board call needs are
108
+ # present and resolvable; it deliberately does NOT hit the network, so it is
109
+ # safe in CI and in `pj audit`.
110
+ smoke() {
111
+ local scope="${1:-root}" rc=0
112
+ require_lib || return 2
113
+ # shellcheck source=/dev/null
114
+ . "$LIB"
115
+
116
+ local provider=""
117
+ provider="$(tp_provider_name 2>/dev/null || true)"
118
+ if [ -z "$provider" ]; then
119
+ echo "smoke: no ticket provider resolved (.project.json ticket_provider.type)" >&2
120
+ rc=1
121
+ else
122
+ echo "smoke: provider = $provider"
123
+ fi
124
+
125
+ local impl
126
+ impl="$(tp_providers_dir 2>/dev/null || true)/${provider}.sh"
127
+ if [ -n "$provider" ] && [ ! -f "$impl" ]; then
128
+ echo "smoke: provider adapter missing: $impl" >&2
129
+ rc=1
130
+ elif [ -n "$provider" ]; then
131
+ echo "smoke: adapter = $impl"
132
+ fi
133
+
134
+ if [ -r "$SCRIPT_DIR/../../../.project.json" ]; then
135
+ echo "smoke: binding = repo-root .project.json"
136
+ else
137
+ echo "smoke: .project.json not found from $SCRIPT_DIR" >&2
138
+ rc=1
139
+ fi
140
+
141
+ if [ "$scope" = "nested" ]; then
142
+ # Nested scope additionally asserts the normalized state vocabulary is
143
+ # intact, since that is what a nested adapter call would transition against.
144
+ local missing=""
145
+ for st in backlog unstarted started in_review completed cancelled; do
146
+ tp_is_valid_state "$st" || missing="$missing $st"
147
+ done
148
+ if [ -n "$missing" ]; then
149
+ echo "smoke: normalized states missing:$missing" >&2
150
+ rc=1
151
+ else
152
+ echo "smoke: states = ok (6 normalized)"
153
+ fi
154
+ fi
155
+
156
+ [ "$rc" -eq 0 ] && echo "smoke: ok ($scope)"
157
+ return "$rc"
158
+ }
159
+
160
+ main() {
161
+ case "${1:---help}" in
162
+ --help|-h|help) usage ;;
163
+ --lifecycle) print_lifecycle ;;
164
+ --smoke) shift; smoke "${1:-root}" ;;
165
+ --provider)
166
+ require_lib || exit 2
167
+ # shellcheck source=/dev/null
168
+ . "$LIB"; tp_provider_name ;;
169
+ *)
170
+ require_lib || exit 2
171
+ # shellcheck source=/dev/null
172
+ . "$LIB"
173
+ tp "$@" ;;
174
+ esac
175
+ }
176
+
177
+ main "$@"
@@ -2,32 +2,32 @@
2
2
  # The agent's actual state (memory, sessions, evolving SOUL) lives in the
3
3
  # ignored local runtime. This file is the immutable contract for this role.
4
4
 
5
- repo: {{ target_repo }}
6
- role: {{ role }}
7
- agent_id: {{ agent_id }}
8
- display_name: "{{ display_name }}"
9
- purpose: "{{ agent_purpose }}"
5
+ repo: {{ target_repo | tojson }}
6
+ role: {{ role | tojson }}
7
+ agent_id: {{ agent_id | tojson }}
8
+ display_name: {{ display_name | tojson }}
9
+ purpose: {{ agent_purpose | tojson }}
10
10
 
11
11
  # Process-local gateway inference target (empty = inherit from shared
12
12
  # ~/.hermes/config.yaml). key_env is a variable NAME only; its secret value is
13
13
  # injected at runtime and must never be written here.
14
14
  model:
15
- provider: "{{ model_provider }}"
16
- name: "{{ model_name }}"
17
- base_url: "{{ model_base_url }}"
18
- api_mode: "{{ model_api_mode }}"
19
- key_env: "{{ model_key_env }}"
15
+ provider: {{ model_provider | tojson }}
16
+ name: {{ model_name | tojson }}
17
+ base_url: {{ model_base_url | tojson }}
18
+ api_mode: {{ model_api_mode | tojson }}
19
+ key_env: {{ model_key_env | tojson }}
20
20
 
21
21
  # Hermes profile name. ~/.hermes/profiles/<name> is a REAL directory managed by
22
22
  # `pj migrate hermes.runtime-singleton`: shared config/auth/skills link to the
23
23
  # fleet root while role-owned state links into ./runtime. Never replace the
24
24
  # named profile with a symlink to runtime.
25
- profile: {{ agent_id }}
25
+ profile: {{ agent_id | tojson }}
26
26
 
27
27
  # Telegram bot identity (one bot per agent — see docs/architecture.md).
28
28
  telegram:
29
29
  provisioning_status: "deferred"
30
- bot_username: "{{ bot_handle }}"
30
+ bot_username: {{ bot_handle | tojson }}
31
31
  bot_id: ""
32
32
 
33
33
  # Slack is opt-in. Credentials live only in runtime/.env; this block records
@@ -44,14 +44,14 @@ slack:
44
44
  # The heartbeat reconciliation pass talks ONLY to this via .scripts/lib/ticket-provider.sh;
45
45
  # swapping providers is a one-line change here. Filled in by 42-ticket-provider.sh.
46
46
  ticket_provider:
47
- name: {{ ticket_provider }}
47
+ name: {{ ticket_provider | tojson }}
48
48
  board_id: ""
49
49
  board_url: ""
50
50
  {%- if ticket_provider == 'linear' %}
51
51
  team: "" # Linear team key, e.g. DEL (set before first sentinel run)
52
52
  project: "" # optional Linear project name to scope milestones/issues
53
53
  {%- elif ticket_provider == 'plane' %}
54
- workspace: "{{ plane_workspace }}"
54
+ workspace: {{ plane_workspace | tojson }}
55
55
  project: "" # Plane project uuid (set by 42-ticket-provider.sh)
56
56
  {%- elif ticket_provider == 'trello' %}
57
57
  board: "" # Trello board id (set by 42-ticket-provider.sh)
@@ -70,7 +70,7 @@ reconcile:
70
70
  # Plane project (1:1 with this agent) — retained for fleet-registry back-compat.
71
71
  plane:
72
72
  # Empty -> resolved from ~/.config/hermes-agent-template/config.toml [plane].workspace
73
- workspace: "{{ plane_workspace }}"
73
+ workspace: {{ plane_workspace | tojson }}
74
74
  # identifier is set by .scripts/42-ticket-provider.sh after creation (plane only)
75
75
  identifier: ""
76
76
 
@@ -81,16 +81,16 @@ bloodbank:
81
81
  # Quarantine gate: discovery is safe; execution requires explicit activation.
82
82
  enabled: false
83
83
  gateway_scope: fleet
84
- target_agent_id: "{{ agent_id }}"
85
- producer: "hermes-agent:{{ agent_id }}"
84
+ target_agent_id: {{ agent_id | tojson }}
85
+ producer: {{ ("hermes-agent:" ~ agent_id) | tojson }}
86
86
 
87
87
  # Pure-local role-owned state. HERMES_HOME itself is the named profile directory
88
88
  # above; the legacy GitHub identity remains metadata-only for locating old
89
89
  # archives, and provisioning never creates or attaches a submodule.
90
90
  runtime:
91
91
  # Empty owner -> resolved from config.toml [github].runtime_repo_owner.
92
- github_owner: "{{ runtime_repo_owner }}"
93
- github_repo: "{{ runtime_repo }}"
92
+ github_owner: {{ runtime_repo_owner | tojson }}
93
+ github_repo: {{ runtime_repo | tojson }}
94
94
  local_path: "./runtime"
95
95
  checkpoint:
96
96
  cadence: "disabled"