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 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
- │ ├── auto-release.yml
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
- `auto-release.yml` validates version consistency across all manifests (`package.json`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -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":"${PLATFORM:-unknown}"}
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
- cat >> "$LOG_DIR/delegations.log" << JSON
21
- {"event":"SubagentStart","timestamp":"$TIMESTAMP","agent":"$AGENT_NAME","task":"$TASK_DESC"}
22
- JSON
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