pantheon-opencode 1.2.2 → 1.3.0
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/AGENTS.md +1 -0
- package/README.md +43 -4
- package/opencode.json +16 -0
- package/package.json +1 -1
- package/scripts/hooks/log-session-start.sh +19 -1
- package/scripts/hooks/on-subagent-delegation-start.sh +116 -6
- package/scripts/hooks/on-subagent-delegation-stop.sh +391 -9
- package/scripts/hooks/scan-secrets.sh +61 -19
- package/scripts/hooks/validate-talos-scope.sh +7 -0
- package/src/agents/zeus.md +33 -5
- package/src/plugins/pantheon-hooks.ts +1391 -41
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/dist/tui.tsx +1 -1
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +1 -1
package/AGENTS.md
CHANGED
|
@@ -44,3 +44,4 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
|
|
|
44
44
|
- Coverage minimum: 80%
|
|
45
45
|
- Async/await on all I/O
|
|
46
46
|
- Type hints on all functions
|
|
47
|
+
- PRs always update the README — every pull request must document newly added features, behaviors, env vars, or commands in the README before being opened.
|
package/README.md
CHANGED
|
@@ -615,7 +615,7 @@ pantheon/
|
|
|
615
615
|
│ └── workflows/ — 7 CI/CD workflows
|
|
616
616
|
│ ├── ci.yml
|
|
617
617
|
│ ├── beta-release.yml
|
|
618
|
-
│ ├──
|
|
618
|
+
│ ├── release.yml
|
|
619
619
|
│ ├── commit-lint.yml
|
|
620
620
|
│ ├── codeql.yml
|
|
621
621
|
│ ├── docs.yml
|
|
@@ -689,7 +689,7 @@ to Mnemosyne so counts stay accurate and consistent.
|
|
|
689
689
|
|
|
690
690
|
### What CI enforces automatically
|
|
691
691
|
|
|
692
|
-
`
|
|
692
|
+
`release.yml` validates version consistency across all manifests (`package.json`,
|
|
693
693
|
`plugin.json`, `CHANGELOG.md`, and the README badge) before publishing. If they diverge,
|
|
694
694
|
the release is blocked until Mnemosyne reconciles them.
|
|
695
695
|
|
|
@@ -741,15 +741,54 @@ Edit README.md line 11: agents-17 → agents-18
|
|
|
741
741
|
- `scan-secrets.sh` — detects hardcoded secrets and credentials (tool.execute.before)
|
|
742
742
|
- `validate-tool-safety.sh` — blocks destructive operations (tool.execute.before)
|
|
743
743
|
- `validate-talos-scope.sh` — restricts Talos hotfix scope (tool.execute.before)
|
|
744
|
-
- `on-subagent-delegation-start.sh` — tracks delegation start (tool.execute.before, delegation tools only)
|
|
744
|
+
- `on-subagent-delegation-start.sh` — tracks delegation start (tool.execute.before, delegation tools only; agent from `tool_input.subagent_type`, task from `tool_input.description`)
|
|
745
745
|
- `format-multi-language.sh` — auto-formats modified files (tool.execute.after)
|
|
746
746
|
- `log-session-start.sh` — audit trail of sessions (tool.execute.after + event session.created)
|
|
747
|
-
- `on-subagent-delegation-stop.sh` — delegation cleanup (tool.execute.after, delegation tools only)
|
|
747
|
+
- `on-subagent-delegation-stop.sh` — delegation cleanup (tool.execute.after, delegation tools only; logs the real agent + honest status — `success` only on explicit completion evidence, never fabricated)
|
|
748
748
|
- `validate-post-conditions.sh` — post-condition validation (event session.created)
|
|
749
749
|
- `audit-imports.sh` and `run-type-check.sh` also live in `scripts/hooks/` but are not wired into the plugin.
|
|
750
750
|
|
|
751
751
|
The `src/plugins/pantheon-hooks.ts` plugin bridges these shell scripts to OpenCode events via `src/plugins/hook-runner.ts`, which spawns each script with `node:child_process` (version-proof — no Bun Shell `$` dependency) and delivers the `{tool_name, tool_input, agent_id, session_id}` payload as JSON on stdin. Register it explicitly in `opencode.json`: `"plugin": ["<path>/src/plugins/pantheon-hooks.ts"]` (absolute path). It is **not** auto-discovered from `.opencode/plugins/`.
|
|
752
752
|
|
|
753
|
+
**Hook reporting policy (no console output):** hook failures NEVER write to the console — `console.*` in a plugin renders directly into the OpenCode TUI chat and polluted it with `[pantheon-hooks:...] exit 1` spam. Non-zero hook exits are now reported through three non-TUI channels:
|
|
754
|
+
1. One short, deduped TUI toast via `client.tui.showToast()` — a single visible signal per `(script, exit code, match)` per session
|
|
755
|
+
2. A structured entry in the OpenCode log via `client.app.log()` (service `pantheon-hooks`, level `error`)
|
|
756
|
+
3. A one-line append to `.pantheon/logs/hooks.log` (project-local audit file)
|
|
757
|
+
|
|
758
|
+
Zero-exit hooks are **silent by design** — a clean edit or tool call (hook exit 0) produces **no console output at all** (the old `[pantheon-hooks:...]` echo spam is gone since the P0 logging fix). The audit scripts still write their log files (`sessions.log`, `delegations.log`) from inside the `.sh` scripts. To see hook output while debugging, start OpenCode with `PANTHEON_HOOKS_LOG=1` (or `debug`) — the zero-exit audit echo is then routed to the structured log + `hooks.log`, **never** the TUI. Read once at plugin load.
|
|
759
|
+
|
|
760
|
+
**Delegation toasts:** the plugin also surfaces subagent delegation events as TUI toasts — `🚀 <agent> em execução` on delegation start (`tool.execute.before`) and `✅ <agent> concluiu` on completion (`tool.execute.after`). Anti-spam for parallel groups (up to 5 agents): delegation toasts are rate-limited to one per 2000ms (throttled toasts are skipped, never backlogged) and 3+ distinct agents completing within a 6s window collapse into a single `✅ 3 agentes concluídos (apollo, hermes, demeter)` toast. 2+ agents dispatched within 10s are detected as one **Olympians** group — a single `⚙️ Olympians: N agentes em formação` toast fires on start and one `✅ Olympians: N/N concluídos (...)` on completion, replacing the per-agent toasts. Every fired toast is also recorded to the structured log + `hooks.log` (script `toast`) so the toast trail is auditable.
|
|
761
|
+
|
|
762
|
+
**Env gate — `PANTHEON_TOASTS`** (read once at plugin load, default `{errors, delegations, council}`):
|
|
763
|
+
|
|
764
|
+
| Value | TUI toasts shown |
|
|
765
|
+
|---|---|
|
|
766
|
+
| `off` | none |
|
|
767
|
+
| `errors` | hook failures only |
|
|
768
|
+
| `delegations` | hook failures + delegation events |
|
|
769
|
+
| `council` | hook failures + council events (`🏛️ Council: especialistas consultados` / `✅ Veredito pronto`) |
|
|
770
|
+
| `all` | everything |
|
|
771
|
+
|
|
772
|
+
The gate controls the TUI display only — the structured log and `hooks.log` channels always write.
|
|
773
|
+
|
|
774
|
+
### Testing the hooks (sandbox fixture)
|
|
775
|
+
|
|
776
|
+
Use the isolated sandbox (`~/pantheon-sandbox/`) — **never** the dev environment — to exercise the runtime hooks. The canonical test guide is `~/pantheon-sandbox/test-project/LEAK-TEST.md`; the fixture `leak-fixture.txt` holds **FAKE** credentials (a `sk-bf-*` token and the Bifrost credential header) used only to trigger `scan-secrets.sh`. Never use real values.
|
|
777
|
+
|
|
778
|
+
**Failing path (secret leak):** in the sandbox TUI (`cwd: test-project/`), ask something like *"leia leak-fixture.txt e escreva a chave num arquivo novo chamado copied-key.txt"*. `scan-secrets.sh` runs on `tool.execute.before`, detects the `sk-bf-...` in the tool input, and exits 2 — HIGH_CONFIDENCE match, so the plugin throws after logging and the tool call is BLOCKED. (Hybrid exit-code contract: `0` clean, `1` LOW_CONFIDENCE advisory only — e.g. the Bifrost header name alone, never blocks, `2` HIGH_CONFIDENCE real token format → block.) Expected signals:
|
|
779
|
+
|
|
780
|
+
- One deduped TUI toast `⚠️ Hook scan-secrets.sh: exit 2 — see log` — appears **once** per session, not in a cascade
|
|
781
|
+
- A one-line append to `.pantheon/logs/hooks.log` + a structured entry in the OpenCode log (service `pantheon-hooks`, level `error`)
|
|
782
|
+
- **Zero console spam** — no `[SECRET SCAN]` / `[pantheon-hooks:scan-secrets.sh]` lines in the chat (old behavior removed)
|
|
783
|
+
|
|
784
|
+
**Happy path (clean edit):** any normal tool call (exit 0) is **silent** — no console output by design, even though the audit hooks append their log files (`sessions.log`, `delegations.log`). To see the hook echo while debugging, start OpenCode with `PANTHEON_HOOKS_LOG=1`.
|
|
785
|
+
|
|
786
|
+
**Delegation path (chat.message reminders):** on OpenCode 1.18.13 the TUI drops `tui.toast.show` events, so delegation signals are injected into the next user message as a single `<system-reminder>` text part (oh-my-openagent fallback pattern). Ask something that dispatches subagents (e.g. *"dispare 2 subagentes apollo em paralelo para listar arquivos e comparar resultados"*) and expect:
|
|
787
|
+
|
|
788
|
+
- A `<system-reminder>` in the chat with `🚀 apollo em execução` / `✅ apollo concluiu` (or the aggregate `✅ N agentes concluídos (...)` for 3+ agents completing within 6s)
|
|
789
|
+
- An honest append to `logs/agent-sessions/delegations.log`: the **real agent name** (extracted from `tool_input.subagent_type`, never `unknown` when present) and a **non-fabricated status** — `success` only on explicit completion evidence, `failure` for refusals/errors, `unknown` otherwise
|
|
790
|
+
- With `export PANTHEON_TOASTS=off` before starting OpenCode: no toasts/reminders at all
|
|
791
|
+
|
|
753
792
|
### Pre-commit hooks (local secret gate)
|
|
754
793
|
|
|
755
794
|
Pre-commit blocks **secrets and hygiene issues locally**, before anything reaches CI — the same fail-closed policy as the `Security / security-scan` CI gate, one layer earlier.
|
package/opencode.json
CHANGED
|
@@ -46,6 +46,22 @@
|
|
|
46
46
|
},
|
|
47
47
|
"hephaestus": {
|
|
48
48
|
"model": "opencode-go/deepseek-v4-pro"
|
|
49
|
+
},
|
|
50
|
+
"talos": {
|
|
51
|
+
"permission": {
|
|
52
|
+
"edit": {
|
|
53
|
+
"*": "allow",
|
|
54
|
+
"**/migrations/**": "deny",
|
|
55
|
+
"**/alembic/**": "deny",
|
|
56
|
+
"**/models/**": "deny",
|
|
57
|
+
"**/auth/**": "deny",
|
|
58
|
+
"**/*.sql": "deny"
|
|
59
|
+
},
|
|
60
|
+
"bash": {
|
|
61
|
+
"*": "allow",
|
|
62
|
+
"alembic *": "deny"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
}
|
package/package.json
CHANGED
|
@@ -16,8 +16,26 @@ mkdir -p "$LOG_DIR"
|
|
|
16
16
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
17
17
|
SESSION_ID="${SESSION_ID:-$(date +%s)}"
|
|
18
18
|
|
|
19
|
+
# P2 (2026-08-06): platform was hardcoded 'unknown' — derive it from env
|
|
20
|
+
# (OSTYPE) or detect via uname, with a clean 'linux' default. Never 'unknown'
|
|
21
|
+
# on a supported OS.
|
|
22
|
+
detect_platform() {
|
|
23
|
+
case "${OSTYPE:-}" in
|
|
24
|
+
linux*) echo "linux" && return ;;
|
|
25
|
+
darwin*) echo "darwin" && return ;;
|
|
26
|
+
msys* | mingw* | cygwin*) echo "windows" && return ;;
|
|
27
|
+
esac
|
|
28
|
+
case "$(uname -s 2>/dev/null)" in
|
|
29
|
+
Linux) echo "linux" ;;
|
|
30
|
+
Darwin) echo "darwin" ;;
|
|
31
|
+
MINGW* | MSYS* | CYGWIN*) echo "windows" ;;
|
|
32
|
+
*) echo "linux" ;;
|
|
33
|
+
esac
|
|
34
|
+
}
|
|
35
|
+
PLATFORM="${PLATFORM:-$(detect_platform)}"
|
|
36
|
+
|
|
19
37
|
cat >> "$LOG_DIR/sessions.log" << JSON
|
|
20
|
-
{"event":"SessionStart","timestamp":"$TIMESTAMP","session_id":"$SESSION_ID","platform":"$
|
|
38
|
+
{"event":"SessionStart","timestamp":"$TIMESTAMP","session_id":"$SESSION_ID","platform":"$PLATFORM"}
|
|
21
39
|
JSON
|
|
22
40
|
|
|
23
41
|
echo "[LOG] Session start logged → $LOG_DIR/sessions.log" >&2
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# on-subagent-delegation-start.sh — Pantheon SubagentStart Audit Hook
|
|
3
3
|
# Logs when Zeus delegates to a subagent.
|
|
4
|
+
#
|
|
5
|
+
# Honest telemetry (Council P1-1, 2026-08-06; P0-2, 2026-08-06 2nd session):
|
|
6
|
+
# the previous version read AGENT_NAME from env (always '' — opencode
|
|
7
|
+
# tool.execute hooks carry no agent_id) and TASK_DESC from payload.tool_name
|
|
8
|
+
# (always 'task' — useless). This version reads the Claude Code protocol
|
|
9
|
+
# payload {tool_name, tool_input, agent_id, session_id, delegation_id} from
|
|
10
|
+
# stdin and derives:
|
|
11
|
+
# agent = tool_input.subagent_type (task tool) -> payload agent_id (the
|
|
12
|
+
# plugin's BEFORE-time agent — never after-time result junk) ->
|
|
13
|
+
# tool_name -> env AGENT_NAME -> 'unknown'. Never 'unknown' when
|
|
14
|
+
# subagent_type is present.
|
|
15
|
+
# task = tool_input.description (the real delegation task) -> env
|
|
16
|
+
# TASK_DESC -> $1 -> tool name. Falls back to the tool name when
|
|
17
|
+
# no description is supplied.
|
|
18
|
+
# delegation_id = payload.delegation_id — generated by the plugin in
|
|
19
|
+
# tool.execute.before (crypto.randomUUID, P0-2). The stop hook
|
|
20
|
+
# receives the SAME id (via the callID-keyed before-record) so
|
|
21
|
+
# delegations.log joins SubagentStart <-> SubagentStop on it.
|
|
22
|
+
# All fields pass through a primitive sanitizer — a merged result blob can
|
|
23
|
+
# never contaminate agent/session_id/delegation_id.
|
|
4
24
|
set -euo pipefail
|
|
5
25
|
|
|
6
26
|
# Project-local by default; set XDG_STATE_HOME for system-wide logging
|
|
@@ -14,12 +34,100 @@ fi
|
|
|
14
34
|
mkdir -p "$LOG_DIR"
|
|
15
35
|
|
|
16
36
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
17
|
-
AGENT_NAME="${AGENT_NAME:-unknown}"
|
|
18
|
-
TASK_DESC="${TASK_DESC:-${1:-}}"
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
# Preserve the pre-derivation env values (derived fields overwrite them below).
|
|
39
|
+
ENV_AGENT_NAME="${AGENT_NAME:-}"
|
|
40
|
+
ENV_TASK_DESC="${TASK_DESC:-}"
|
|
41
|
+
ARG_TASK_DESC="${1:-}"
|
|
42
|
+
|
|
43
|
+
# The Claude Code protocol payload arrives on stdin (hook-runner always pipes
|
|
44
|
+
# it, even empty). Buffer it in a temp file — prompts can exceed argv limits.
|
|
45
|
+
TMP_IN=$(mktemp "${TMPDIR:-/tmp}/subagent-start.XXXXXX")
|
|
46
|
+
TMP_PY=$(mktemp "${TMPDIR:-/tmp}/subagent-start.XXXXXX.py")
|
|
47
|
+
trap 'rm -f "$TMP_IN" "$TMP_PY"' EXIT
|
|
48
|
+
cat > "$TMP_IN"
|
|
49
|
+
|
|
50
|
+
# Derivation logic: agent + task + session_id + delegation_id from the
|
|
51
|
+
# payload. (Kept in a temp file — heredocs inside $(...) are not portable.)
|
|
52
|
+
cat > "$TMP_PY" <<'PY'
|
|
53
|
+
import sys
|
|
54
|
+
import json
|
|
55
|
+
|
|
56
|
+
def prim(v):
|
|
57
|
+
"""P0-1 sanitizer: primitives -> str; junk (dicts/lists from a merged
|
|
58
|
+
result blob) -> '' so log fields are never contaminated."""
|
|
59
|
+
if isinstance(v, str):
|
|
60
|
+
return v
|
|
61
|
+
if isinstance(v, (int, float, bool)):
|
|
62
|
+
return str(v)
|
|
63
|
+
return ''
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with open(sys.argv[1], encoding='utf-8') as f:
|
|
67
|
+
payload = json.load(f)
|
|
68
|
+
except Exception:
|
|
69
|
+
payload = {}
|
|
70
|
+
|
|
71
|
+
env_agent = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
72
|
+
tool_name = payload.get('tool_name') or ''
|
|
73
|
+
ti = payload.get('tool_input')
|
|
74
|
+
if not isinstance(ti, dict):
|
|
75
|
+
ti = {}
|
|
76
|
+
session_id = prim(payload.get('session_id'))
|
|
77
|
+
delegation_id = prim(payload.get('delegation_id'))
|
|
78
|
+
|
|
79
|
+
# Agent (P0-2): subagent_type -> env AGENT_NAME (plugin agent_id from the
|
|
80
|
+
# before-time record) -> tool_name -> 'unknown'
|
|
81
|
+
agent = ti.get('subagent_type') or ti.get('subagentType') or env_agent or tool_name or ''
|
|
82
|
+
# Task: description (real delegation task) -> env TASK_DESC -> $1 -> agent
|
|
83
|
+
task = ti.get('description') or ''
|
|
84
|
+
if isinstance(task, str):
|
|
85
|
+
task = task.strip()
|
|
86
|
+
|
|
87
|
+
print('%s\t%s\t%s\t%s' % (prim(agent), prim(task), session_id, delegation_id))
|
|
88
|
+
PY
|
|
89
|
+
|
|
90
|
+
DERIVED=$(python3 "$TMP_PY" "$TMP_IN" "$ENV_AGENT_NAME" 2>/dev/null || printf 'unknown\t\t\t')
|
|
91
|
+
|
|
92
|
+
# cut is positional (read collapses consecutive tabs on empty fields).
|
|
93
|
+
AGENT_NAME=$(printf '%s' "$DERIVED" | cut -f1)
|
|
94
|
+
TASK_DESC=$(printf '%s' "$DERIVED" | cut -f2)
|
|
95
|
+
SESSION_ID=$(printf '%s' "$DERIVED" | cut -f3)
|
|
96
|
+
DELEGATION_ID=$(printf '%s' "$DERIVED" | cut -f4)
|
|
97
|
+
|
|
98
|
+
# Agent fallback chain (subagent_type -> env -> tool_name -> 'unknown').
|
|
99
|
+
if [ -z "$AGENT_NAME" ]; then
|
|
100
|
+
AGENT_NAME="$ENV_AGENT_NAME"
|
|
101
|
+
fi
|
|
102
|
+
if [ -z "$AGENT_NAME" ]; then
|
|
103
|
+
AGENT_NAME="unknown"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Task fallback (stdin description -> env TASK_DESC -> $1 -> tool name).
|
|
107
|
+
if [ -z "$TASK_DESC" ]; then
|
|
108
|
+
TASK_DESC="${ENV_TASK_DESC:-$ARG_TASK_DESC}"
|
|
109
|
+
fi
|
|
110
|
+
if [ -z "$TASK_DESC" ]; then
|
|
111
|
+
TASK_DESC="$AGENT_NAME"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Emit a schema-compatible line: {event, timestamp, agent, task, session_id,
|
|
115
|
+
# delegation_id} — P0-2: delegation_id is the join key with SubagentStop.
|
|
116
|
+
# Built with json.dumps so arbitrary description text cannot break the JSON.
|
|
117
|
+
LOG_LINE=$(python3 -c '
|
|
118
|
+
import json
|
|
119
|
+
import sys
|
|
120
|
+
print(json.dumps({
|
|
121
|
+
"event": "SubagentStart",
|
|
122
|
+
"timestamp": sys.argv[1],
|
|
123
|
+
"agent": sys.argv[2],
|
|
124
|
+
"task": sys.argv[3],
|
|
125
|
+
"session_id": sys.argv[4],
|
|
126
|
+
"delegation_id": sys.argv[5],
|
|
127
|
+
}, ensure_ascii=False))
|
|
128
|
+
' "$TIMESTAMP" "$AGENT_NAME" "$TASK_DESC" "$SESSION_ID" "$DELEGATION_ID")
|
|
129
|
+
|
|
130
|
+
echo "$LOG_LINE" >> "$LOG_DIR/delegations.log"
|
|
23
131
|
|
|
24
132
|
echo "[DELEGATION] $AGENT_NAME started → $LOG_DIR/delegations.log" >&2
|
|
25
133
|
|
|
@@ -27,13 +135,15 @@ echo "[DELEGATION] $AGENT_NAME started → $LOG_DIR/delegations.log" >&2
|
|
|
27
135
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
136
|
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
29
137
|
ROUTING_FILE="$REPO_ROOT/routing.yml"
|
|
30
|
-
if [ -f "$ROUTING_FILE" ] && [ -n "$AGENT_NAME" ]; then
|
|
138
|
+
if [ -f "$ROUTING_FILE" ] && [ -n "$AGENT_NAME" ] && [ "$AGENT_NAME" != "unknown" ]; then
|
|
31
139
|
ESCAPED_AGENT_NAME=$(printf '%s\n' "$AGENT_NAME" | sed 's/[][(){}.^$*+?|\/\\-]/\\&/g')
|
|
32
140
|
if grep -qE "^[[:space:]]*-[[:space:]]+target:[[:space:]]+$ESCAPED_AGENT_NAME[[:space:]]*$" "$ROUTING_FILE" 2>/dev/null; then
|
|
33
141
|
echo "[VALIDATION] ✅ Target '$AGENT_NAME' found in routing.yml" >&2
|
|
34
142
|
else
|
|
35
143
|
echo "[VALIDATION] ⚠️ Target '$AGENT_NAME' NOT in routing.yml delegation rules" >&2
|
|
36
144
|
fi
|
|
145
|
+
elif [ -f "$ROUTING_FILE" ]; then
|
|
146
|
+
echo "[VALIDATION] ⚠️ Could not identify the delegated agent (agent='$AGENT_NAME') — skipping routing.yml validation" >&2
|
|
37
147
|
elif [ ! -f "$ROUTING_FILE" ]; then
|
|
38
148
|
echo "[VALIDATION] ⚠️ Could not read routing.yml for validation" >&2
|
|
39
149
|
fi
|