@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.
- package/README.md +72 -0
- package/dist/index.js +4510 -2485
- package/dist/mcp-server.js +4166 -1959
- package/dist/prompt.js +404 -0
- package/package.json +7 -5
- package/templates/commonproject/copier.yml +6 -1
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +14 -50
- package/templates/hermes-agent/copier.yml +8 -11
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
- package/templates/hermes-agent/template/.scripts/01-config.sh +9 -0
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +68 -4
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +73 -43
- package/templates/hermes-agent/template/.scripts/80-registry.sh +6 -0
- package/templates/hermes-agent/template/.scripts/_lib.sh +62 -6
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +13 -1
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +734 -0
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +1 -1
- package/templates/hermes-agent/template/SOUL.md.jinja +44 -8
- package/templates/hermes-agent/template/hermes.jinja +20 -8
- package/templates/hermes-agent/template/momo.jinja +177 -0
- 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
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`
|
|
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 (
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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:
|
|
9
|
-
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:
|
|
16
|
-
name:
|
|
17
|
-
base_url:
|
|
18
|
-
api_mode:
|
|
19
|
-
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:
|
|
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:
|
|
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:
|
|
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:
|
|
85
|
-
producer: "hermes-agent:
|
|
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:
|
|
93
|
-
github_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"
|