Skip to content

feat(screen-companion): skeleton skill (M1) + guided-setup config - #794

Merged
qingyun-wu merged 11 commits into
mainfrom
feat/screen-companion-skill
May 18, 2026
Merged

qingyun-wu merged 11 commits into
mainfrom
feat/screen-companion-skill

Conversation

@qingyun-wu

@qingyun-wu qingyun-wu commented May 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

v0 of the screen-companion skill — closes the gap from screen_companion.md (owner-doc): capabilities exist, but Sutando doesn't know the purpose, so every session you have to re-narrate intent.

Implements Model M1 from the design discussion: master skill + N declarative configs in configs/*.yaml. Each config encodes one use case. Adding a new use case = drop in a YAML — no code change.

v0 wired-up config: guided-setup — proactive + goal-directed mode for navigating an unfamiliar UI (e.g. Discord dev portal, Stripe dashboard, AWS console). Owner names the goal at activation, shares screen, Sutando narrates the next concrete step in real time.

Schema swap from the initial pair-read-paper choice → guided-setup (commit 41a670a) reflects the design discussion in screen_companion.md: input 3 ("setting up a Discord bot") is the more visceral demo angle than paper-reading, and the use case is broader (everybody fights some dev portal once a quarter).

What ships in this PR

File Purpose
skills/screen-companion/SKILL.md Skill manifest doc. Model, configs, how to add, trust + scope, privacy.
skills/screen-companion/manifest.json Skill-loader entry — enabled: true, access_tier: owner, points at tools.ts.
skills/screen-companion/configs/guided-setup.yaml v0 wired-up config — activation triggers, vision push@700ms, system-prompt overlay, tool allow-list, goal template.
skills/screen-companion/tools.ts Voice-side inline tool activate_screen_companion(mode, goal). Gemini calls this on activation phrases — returns prompt overlay + tools_allow + vision config to swap into the session.
skills/screen-companion/scripts/load-config.ts Shared YAML loader + validator (used by both the CLI and the inline tool).
skills/screen-companion/scripts/activate.ts CLI: --list to discover configs, --config <name> --goal "..." to preview activation without entering the mode.

What's intentionally NOT in this PR

  • No sibling configs (pair-debug, pair-review-code). The schema gets validated against guided-setup first; siblings land once integration shape is locked. Sketched in screen_companion.md, low effort per config (~30min each).
  • No voice-phrase intent classifier. Activation today is via the tool — Gemini picks the right mode when it hears one of the configured voice_phrases. A separate intent classifier on the voice-agent side is a different concern; if multi-mode disambiguation gets thorny, that's the followup.

Smoke test

$ npx tsx skills/screen-companion/scripts/activate.ts --list
Configs:
  ✓ guided-setup  — "help me set this up"

$ npx tsx skills/screen-companion/scripts/activate.ts --config guided-setup --goal "find the bot token in the Discord developer portal"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Screen Companion — activating: guided-setup
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Activation triggers:
  voice phrases:  "help me set this up", "guide me through this", …
  button label:   "Guide me"
  CLI alias:      guided-setup

Vision:
  mode:           push
  cadence:        700ms

Tools allowed (3):
  • vision_query
  • take_note
  • look_up_reference

Goal: Helping the user: find the bot token in the Discord developer portal

System prompt overlay:
[the full mode prompt — see configs/guided-setup.yaml]

Inline tool module loads cleanly (verified via npx tsx -e "import('./skills/screen-companion/tools.ts')" → activate_screen_companion). CI green on tsc + tests.

Live demo validation

Owner ran a real 20-minute end-to-end demo of guided-setup on 2026-05-17 — a Discord-bot setup walkthrough in the Discord Developer Portal. ~46 turns sustained, two activations across the session. Verdict from owner: "the demo is working decently." The session shook out three bugs that were fixed in-branch before this PR closed:

  • PyYAML missing under the LaunchAgent PATH (commit 298abb0) — parseYaml shelled to python3 and assumed PyYAML was importable; LaunchAgent's PATH found Homebrew python (no PyYAML). Now tries /usr/bin/python3 → /opt/homebrew/bin/python3 → python3 in order.
  • Share-start nudge for discoverability (commits 626a18f → c8f19c3) — owner observed share-then-silence forced you to know the activation phrase. Vision-on now injects a one-sentence prompt that names "screen-companion mode" + available config(s) by name and asks the user's goal. Live-tested: nudge fires, goal gets routed to activate_screen_companion.
  • Goal-priming hallucination (commit 4fa6de6) — fresh-session diagnostic proved Gemini was asserting "you're on the Discord Developer Portal" even when the visible page was unrelated, anchored by the goal text. Added a GROUNDING RULE to the overlay: name what you ACTUALLY see (page title, dominant UI, page kind) BEFORE naming any step; if visible page doesn't match the goal's destination, ask the user to navigate rather than fabricate. Verified live — Gemini now says e.g. "I can see you're on a blank Chrome tab right now. Please navigate to ... and let me know when you're there."

How activation wires through (end-to-end)

  1. Voice-agent boots → inline-tools.ts:920+ skill-loader discovers skills/screen-companion/manifest.json, sees enabled: true, imports ./tools.ts, registers activate_screen_companion as an inline tool with access_tier: owner.
  2. User says one of voice_phrases → Gemini calls activate_screen_companion(mode="guided-setup", goal="…").
  3. Tool reads configs/guided-setup.yaml, returns { instructions, tools_allow, vision_mode, vision_cadence_ms, activation_message }.
  4. Gemini speaks activation_message, then treats instructions as its operating system prompt for the rest of the session.
  5. If vision_mode=push and screen-share isn't already streaming, Gemini asks the user to start it (the tool does not toggle vision itself — owner-driven).

Design choices worth flagging

  • YAML parsing via python3 subprocess, matching src/oc-profile-catalog.ts (PR feat(oc-profile-catalog): load OpenCompanion device-profiles yaml into DeviceProfile #759) — avoids adding js-yaml as an npm dep for one parser call.
  • tools_allow is a whitelist, not a blacklist. Configs can only narrow the tool surface, never grant tools the VoiceSession doesn't already expose. Documented in SKILL.md trust + scope.
  • No SUTANDO_WORKSPACE writes from the loader. Only reads configs/*.yaml (source-tree files; legitimate REPO_DIR access per docs/workspace-contract.md from PR fix(workspace-vs-repo): honor SUTANDO_WORKSPACE across runtime-state readers #793). Per-session transcripts + user-requested notes are workspace writes via existing infra (data/, notes/), not new paths.
  • Privacy default = no screenshots, no recordings on disk. Vision frames cross the wire to Gemini Live and are discarded. Same trust boundary as phone-conversation/. Save-this-screenshot is opt-in per request.

Followup PRs

  1. feat(screen-companion): pair-debug + pair-review-code configs — once guided-setup proves the schema in real use, add the two siblings (each ~30min: just a new YAML).
  2. feat(screen-companion): voice-phrase intent classifier — only if owner finds Gemini picking the wrong mode on overlapping phrases. Separate concern.

Open question

screen_companion.md open-question #6 was implicitly resolved by commit 41a670a (swap to guided-setup). The remaining open question is #5 — packaging model M1 vs M2 vs M3 vs hybrid (still M1 in this PR; doc proposes "start M1, promote individual use cases to M2 when they earn a demo clip"). Owner's call.

🤖 Generated with Claude Code

qingyun-wu and others added 2 commits May 17, 2026 16:09
v0 skeleton for the screen-companion skill. Closes nothing yet — this
PR ships the *primitive*, not the working feature. Followup PR wires
the loaded config into voice-agent.ts.

What's in this PR:

- skills/screen-companion/SKILL.md — describes the skill, the M1 model
  (master skill + N declarative configs), how to add a new use case,
  trust + scope boundary.

- skills/screen-companion/configs/pair-read-paper.yaml — the first
  use-case config. Demonstrates the full schema: activation triggers
  (voice phrases + button label + CLI alias), vision_mode + cadence,
  system_prompt_overlay with domain priors + don't-list, tools_allow
  whitelist, goal_template for optional activation-time fill-in.

- skills/screen-companion/scripts/activate.ts — loader + activation
  printer. v0 scope: discover configs in configs/*.yaml, validate
  required fields, load one by name, print what *would* happen when
  it's activated. No voice-agent wiring yet — intentional, so the
  config schema can crystallize against a real concrete config
  (pair-read-paper) before the integration shape is committed.

CLI:
  npx tsx skills/screen-companion/scripts/activate.ts --list
  npx tsx skills/screen-companion/scripts/activate.ts --config pair-read-paper
  npx tsx skills/screen-companion/scripts/activate.ts --config pair-read-paper --goal "..."

Picks M1 ("master skill + declarative configs") from screen_companion.md.
Discussion + tradeoffs vs M2 (per-use-case sub-skills) + M3 (SDK) live
in that doc.

YAML parsing via python3 subprocess (same pattern as src/oc-profile-catalog.ts);
avoids adding js-yaml as an npm dep.

Next PR (followup):
- Wire system_prompt_overlay onto the active VoiceSession.
- Restrict the tool surface to tools_allow at activation time.
- Hook vision push/pull cadence.
- Add pair-debug + pair-review-code configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…setup

Owner-picked 2026-05-17: the v0 first-shipped config is `guided-setup`
(proactive + goal-directed — Sutando narrates next steps for a UI
config task), not `pair-read-paper`. More visceral demo angle: "Sutando
helps me set up a Discord bot I've never configured before" vs. "Sutando
reads a paper with me."

Changes in this commit:
- Add skills/screen-companion/configs/guided-setup.yaml — the new v0
  config. PROACTIVE shape: tighter vision cadence (700ms vs 1000ms),
  prompt overlay narrates next steps continuously, explicit DO NOT
  click/type guidance (Sutando narrates; user keeps the action).
- Drop skills/screen-companion/configs/pair-read-paper.yaml — was
  shipping as the v0 reference config but the demo angle is weaker;
  owner-clarified the framing should center on "help me set up
  something I've never done before."
- Update SKILL.md table + run example + roadmap to reflect guided-setup
  as the shipped config; pair-read-paper dropped (siblings pair-debug
  + pair-review-code still tracked in screen_companion.md as future).
- Update activate.ts doc header + CLI example to use guided-setup.
- Add Privacy + data handling section to SKILL.md: default DOES NOT
  take screenshots, record video, or persist frames; frames are sent
  to Gemini Live for real-time understanding and discarded; text
  transcripts are persisted (conversation-server precedent); user
  has to explicitly opt-in per request to save screenshots/recordings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qingyun-wu qingyun-wu changed the title feat(screen-companion): skeleton skill (M1) + pair-read-paper config feat(screen-companion): skeleton skill (M1) + guided-setup config May 17, 2026
@qingyun-wu
qingyun-wu force-pushed the feat/screen-companion-skill branch from 393f20f to 41a670a Compare May 17, 2026 23:09
@qingyun-wu
qingyun-wu marked this pull request as ready for review May 17, 2026 23:09
@qingyun-wu
qingyun-wu marked this pull request as draft May 17, 2026 23:09
qingyun-wu and others added 5 commits May 17, 2026 16:18
Closes the wiring gap left by the v0 skeleton — voice can now activate
guided-setup mode directly. Earlier passes shipped the YAML config + CLI
loader; this pass adds the missing piece:

- manifest.json — skill is enabled, points to tools.ts
- tools.ts — exports activate_screen_companion tool (loads config, returns
  prompt overlay + activation_message + vision hint as a structured payload)
- scripts/load-config.ts — shared loader used by both the CLI activate.ts
  and the inline tool (was duplicated; now single source of truth)
- scripts/activate.ts — refactored to import from load-config.ts (-80 lines)
- SKILL.md — drops the "v0 skeleton — load + print only" caveat, adds the
  voice activation phrases + the wire-through explanation

Smoke test (matches what voice will see):
  npx tsx -e "import('./skills/screen-companion/tools.ts').then(...)"
  → status: 'activated', mode: 'guided-setup', goal/instructions/tools_allow
    all populated, activation_message ready to speak.

Architecture: tool lives in the skill (per CLAUDE.md decision guide —
self-contained feature). Voice-agent picks it up via the existing skill
manifest loader (same pattern as gws-gmail-voice). No changes to
src/inline-tools.ts or src/voice-agent.ts.

Voice-agent restart required to register the new tool. Configs themselves
are read fresh per activation, so adding more configs in later PRs does
not require another restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Voice-agent runs under a LaunchAgent whose PATH is
`/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin`. Homebrew's python3 wins
that lookup but does not ship PyYAML, so `python3 -c 'import yaml; ...'`
in parseYaml() failed every activation with ModuleNotFoundError. The
tool returned an error and Gemini fell back to default behavior — silent
to the owner aside from "screen companion failed."

Try a list of python3 candidates (system → homebrew → PATH default), use
the first that can run the yaml import. macOS's `/usr/bin/python3` ships
PyYAML, so the system Python is the usual winner under the LaunchAgent
PATH. Aggregate per-binary errors into the thrown message for diagnosis
when all candidates miss.

Verified under the LaunchAgent's PATH env: activation now succeeds.
The existing screen-share-started system note tells the model to say "I
can see your screen now" but stops there — so users who don't already
know the activation phrases (e.g. "help me set this up" for
guided-setup) share their screen and get no signal that pre-built
companion modes exist. Add a one-line, mode-agnostic nudge: "Want me
to help you through something?" If they describe a goal that fits any
configured mode, the model routes via `activate_screen_companion`.

Mode-agnostic on purpose — the prompt does not list configs by name, so
it won't age as new ones land (pair-debug, pair-review-code planned per
screen_companion.md). The model already knows the catalog from the
`activate_screen_companion` tool's description (which discovers configs
at boot), so listing them in the nudge would be redundant.
…icitly

First attempt (626a18f) shipped a mode-agnostic nudge ("Want me to help
you through something?") that did not surface the screen-companion
catalog. Owner test 2026-05-17 17:28 PT: routing worked (goal → mode)
but discoverability didn't — owner reported "I do not hear the nudge
about screen companion mode."

Updated injected prompt instructs Gemini to deliver ONE sentence that
(a) acknowledges the screen, (b) names "screen-companion mode"
explicitly, (c) names the available configs by name (Gemini reads them
from the activate_screen_companion tool description, which is
discoverConfigs() at boot — so new YAMLs surface automatically), (d)
asks the user's goal.

Example shape: "I can see your screen — screen-companion mode is
available with guided-setup for UI walkthroughs. What are you trying
to do?"
Owner test 2026-05-17 17:34 PT: in a FRESH voice session with no
chat history, Gemini activated guided-setup for "set up a Discord bot"
and asserted "you're on the Discord Developer Portal" + "I see your
cursor hovering over those existing apps" — when the user was
demonstrably NOT on the dev portal. Goal-priming overrode actual
vision input. (Verified via fresh-session diagnostic — chat-history
anchoring was ruled out.)

Add a GROUNDING RULE section at the top of the overlay:
- Name what you ACTUALLY see (page title/URL/breadcrumb, dominant UI
  elements, overall page kind) BEFORE naming any step.
- If the visible page doesn't match the goal's destination, do NOT
  assert the user is on the right page — say what you see and ask them
  to navigate.
- Never fabricate UI elements; if a label is illegible, ask the user
  to read it.
- Goal text is a hint about WHERE the user is going, NOT proof of
  where they ARE. Reconcile goal vs visible-screen as two separate
  facts every turn.

Configs are loaded fresh on each activation — next re-activation picks
this up without a voice-agent restart.
@qingyun-wu
qingyun-wu marked this pull request as ready for review May 18, 2026 00:42

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cold review — second pass after Susan. Read PR body, full diff, walked the call path locally (checked out branch, ran npx tsx scripts/activate.ts --list ✓, verified import('./tools.ts') ✓), and cross-checked the skill-loader contract in src/inline-tools.ts:937-991.

What's solid

  • Skill is genuinely self-contained: skills/screen-companion/{manifest.json,SKILL.md,tools.ts,scripts/load-config.ts,configs/}. Core boots fine with this skill removed (loader's if (!manifest.enabled) continue guard at src/inline-tools.ts:971 handles it cleanly). Matches the architecture rule "each skill is optional."
  • YAML-via-python3 mirrors the oc-profile-catalog.ts precedent (commit 8594bdf) — no js-yaml dep churn for one parser call. The PyYAML-not-found fallback chain in scripts/load-config.ts:33-55 is well-motivated by the cited LaunchAgent PATH bug and the error string surfaces every candidate tried.
  • M1 declarative-config shape is the right starting point; siblings become 30-min YAML drops.
  • Live 20-min validation with three pre-PR bugfix commits (PyYAML, share-start nudge, grounding rule) is real evidence, not just a smoke test.
  • Privacy posture in SKILL.md (frames discarded, transcripts only) is consistent with phone-conversation/ precedent.

Findings

Must-fix

  1. tools_allow references three tools that do not exist anywhere in the codebase. configs/guided-setup.yaml:83-86 whitelists vision_query, take_note, look_up_reference. Greppable: zero matches outside this skill. The actual vision tools are start_vision / stop_vision / send_vision_frame (src/vision-tools.ts:391,426,469); there is no take_note or look_up_reference registered at all. Combined with finding #2, the allow-list is currently fiction — Gemini will see "tools_allow: [vision_query, take_note, look_up_reference]" in the activation payload and either (a) hallucinate calls to non-existent tools (gets a tool-not-found error) or (b) ignore the field and use whatever's actually in scope. Pick one: either rename to real tools, or land the missing tools, or document this PR as "schema only, tool wiring next."

  2. tools_allow is not enforced. tools.ts:67 returns tools_allow as a string list in the response payload. Nothing actually scopes the VoiceSession's tool surface — Gemini is asked to self-restrict via the _note field at tools.ts:72. The PR body and SKILL.md:103-104 both describe it as enforced ("allow-list is enforced at activation time; configs cannot grant tools the active VoiceSession doesn't already expose"). That's not what the code does. Either ship the enforcement now, or downgrade the SKILL.md claim from "enforced" to "advisory" until the next PR — false security claims age badly.

  3. Feature-specific knowledge leaked into core (src/vision-tools.ts:254-256). The injected system note hardcodes the strings "screen-companion mode" and "guided-setup". Per CLAUDE.md decision guide ("core services must NOT contain feature-specific logic … skills are optional"), if a user disables/removes this skill, the core vision-tools.ts now lies to Gemini ("screen-companion mode is available with guided-setup"). Fix: read the live skill manifest list at injection time and only mention modes that the skill-loader actually registered, OR move the prompt-overlay augmentation into the skill itself (a hook the vision tool calls — same pattern as inline tool registration).

Nice-to-have

  1. renderGoal runs before the goal-required guard (tools.ts:46-52). Logic works, but if goal is undefined and goal_template is set, line 47 produces a string with literal {goal} unreplaced before the guard at line 48 short-circuits. Reorder: check config.goal_template && !goal first, then renderGoal. Tiny.

  2. validateConfig doesn't validate tools_allow is a string array (scripts/load-config.ts:58-80). YAML with tools_allow: "vision_query" (string instead of list) would pass validation, get cast to ScreenCompanionConfig, and crash at the tools_allow.length access in activate.ts:43 with an unhelpful error. Add Array.isArray(c.tools_allow) && c.tools_allow.every(t => typeof t === 'string'). Cheap.

  3. vision_cadence_ms has no upper bound check. A YAML typo (70000 instead of 700) would silently configure 70-second frames. Validate sensible range (e.g. 100–5000ms) at load time.

Nit

  1. activate.ts:23 — next?.startsWith('--') ? '' : next returns empty-string for --config --goal foo, which then trips line 88's if (!name) truthiness check correctly, but the user gets the generic usage error rather than "missing value for --config". Minor UX.

  2. SKILL.md:48 — bash scripts/activate.ts --list should be npx tsx scripts/activate.ts --list (matches the smoke-test block above).

Verdict

Request changes, primarily for findings #1 and #2 — the tools_allow claim in SKILL.md doesn't match the code, and the YAML references tools that don't exist. Both are quick fixes (rename allow-list entries to real tool names + downgrade SKILL.md from "enforced" to "advisory until next PR"). Finding #3 is a CLAUDE.md architecture-rule violation worth fixing in this PR rather than a followup — the leak is one transport.sendContent call.

Skeleton itself is the right shape and the live demo validates the integration end-to-end. Once #1–#3 are addressed, this is ready to land.

…o advisory

sonichi cold-review on PR #794 flagged: SKILL.md asserts the tool allow-
list is "enforced at activation time; configs cannot grant tools the
active VoiceSession doesn't already expose" — but tools.ts:67 just
returns the field in the activation payload as a string list. The
_note instructs Gemini to self-restrict; there is no path in the
bodhi-realtime-agent layer that scopes the live tool surface.

Update the Trust + scope section to match reality: advisory in v0,
hard enforcement deferred to a follow-up PR. The risk surface
("worst case is Gemini calls a tool you didn't intend") is named
explicitly so readers can calibrate.

Code unchanged — the field still functions as the documented intent
and Gemini does self-restrict in observed sessions; only the doc claim
was overstating.
…#5, #6, #8)

From sonichi cold-review on PR #794 — uncontroversial nice-to-haves
that don't depend on the scope decisions for findings #1–#3.

- #5: validateConfig now checks tools_allow is a string array. A YAML
  with `tools_allow: "vision_query"` (string instead of list) used to
  pass validation and crash later at `.length` access with a confusing
  error. Now fails fast at load with a clear message.
- #6: vision_cadence_ms bounded to 100–5000ms. A typo like 70000 (70s)
  would silently produce useless frame cadence; 50ms would saturate the
  network. The 100–5000 range covers every plausible mode today.
- #8: SKILL.md typo — "bash scripts/activate.ts --list" → "npx tsx ..."
  (matches the smoke-test block above; bash can't execute the .ts).

No behavior change for the live guided-setup config (verified by
--list and tsc clean).
…ichi #1)

PR #794 review finding #1 — tools_allow lists three tools that don't
exist anywhere in the codebase (zero grep matches outside this skill).
sonichi's three suggested fixes were: rename to real tools, land the
missing tools, or document as schema-only. Going with the documentation
path for v0 — least-risky of the three, reversible, and consistent
with the truthfulness fix in eff1286 that already downgrades the
allow-list claim in SKILL.md from "enforced" to "advisory".

guided-setup as shipped relies only on push-mode vision frames + Gemini
Live's conversational capability — no tool calls are actually required
to operate. The PLANNED comment captures design intent without
fabricating wiring that doesn't exist; a follow-up PR registers real
tools and Gemini gets a real allow-list to follow.

Code unchanged. CI green expected — pure YAML comments.
…ledge from core (sonichi #3)

PR #794 review finding #3: src/vision-tools.ts:254-256 hardcoded the
strings "screen-companion mode" and "guided-setup" in the screen-share-
started system note. Per CLAUDE.md decision guide, core services must
not contain feature-specific logic; if a user removed the screen-
companion skill, vision-tools.ts would lie to Gemini about modes that
no longer exist.

Refactor:

- src/vision-tools.ts now exposes `registerVisionOnContributor(fn)`.
  When push mode starts, the injected note is built from a generic base
  note + each contributor's output. With zero contributors registered,
  the note is generic and mentions no modes.
- skills/screen-companion/tools.ts registers a contributor at module-
  load time. The contributor calls discoverConfigs() at injection time,
  so new YAMLs in configs/ surface automatically without code changes.
- If the skill is disabled (manifest.enabled=false) or removed, the
  skill-loader at src/inline-tools.ts:937 never imports tools.ts, the
  contributor never registers, and vision-tools.ts emits only the base
  note — exactly matches CLAUDE.md "skills are optional."

Behavioral equivalence: with screen-companion enabled (default), the
injected note now contains the same essential content as before — just
sourced from the contributor instead of hardcoded.

Verified: tsc clean. Smoke test confirms contributor count goes 0→1
when the skill module is imported. Voice-agent restarted to pick up
the new module.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review after eff1286 / a3883b1 / f3b1598 / b30e4d6. Walked the updated diff against my cold-review punch list.

What's now solid

  • Finding #2 (SKILL.md "enforced" → "advisory") — eff1286 rewrites the Trust + scope section honestly: `tools_allow` is advisory in v0, hard enforcement deferred, risk surface ("Gemini calls a tool you didn't intend") named explicitly. False security claim is gone. ✓
  • Finding #3 (core knows about skill-specific strings) — b30e4d6 is the right shape: `src/vision-tools.ts:159-170` adds `registerVisionOnContributor()`; the push-start note (lines 285-298) builds from a generic base + each registered contributor. `skills/screen-companion/tools.ts:24-35` registers the screen-companion contributor at module-load time. If the skill is disabled, no contributor fires and the base note is generic — matches CLAUDE.md "skills are optional." ✓
  • Finding #5 (`tools_allow` validation) — a3883b1 adds `Array.isArray + every typeof === 'string'` at `load-config.ts:90-92`. ✓
  • Finding #6 (`vision_cadence_ms` range) — a3883b1 bounds it to 100-5000ms at `load-config.ts:84-88`. ✓
  • Nit #8 (SKILL.md bash typo) — fixed to `npx tsx` at SKILL.md:74. ✓

Finding #1 — accepted with caveats

f3b1598 takes documentation path (option 3 from my original review): the three names stay in the YAML but with a long inline comment explaining they're PLANNED, not registered. Defensible given #2 is now "advisory" — the field is design intent, not security.

Two residual leaks of the same false-tool names:

  • `tools.ts:102` — error-response hint still says "fall back to regular vision_query / take_note / look_up_reference behavior." Gemini sees this on activation failures and may try to call non-existent tools.
  • `SKILL.md:114` — Privacy section says transcripts are persisted "via the `take_note` tool" — implies it exists today.

Both are doc/string nits, not behavior blockers. Worth a one-line follow-up commit but not a re-request-changes.

Remaining nice-to-have

  • #4 (renderGoal call order) not addressed. `tools.ts:68-73` still calls `renderGoal(config, goal)` BEFORE the `goal_template && !goal` guard. Today's effect: the returned (unused) string carries literal `{goal}`, then the guard short-circuits with an error — works, but the wasted call is the kind of bug that lands a stray prompt-injected `{goal}` in a future code path. Trivial swap of lines 68 ↔ 69-73.

Verdict: LGTM

All three must-fix items are addressed. The renderGoal reorder and the two leftover false-tool-name strings are cleanup, not blockers — ship and clean up in a follow-up. Skeleton + guided-setup is ready to land.

@qingyun-wu
qingyun-wu merged commit e588d3f into main May 18, 2026
2 checks passed
@qingyun-wu
qingyun-wu deleted the feat/screen-companion-skill branch May 18, 2026 01:47
qingyun-wu added a commit that referenced this pull request May 18, 2026
Empty commit. Previous push of b6303e3 landed on the remote branch
but GitHub did not fire a pull_request synchronize event — no CI
ran on b6303e3 and PR head still showed b30e4d6. Empty-commit push
typically forces the event to fire.
qingyun-wu added a commit that referenced this pull request May 18, 2026
…iew #4 + 2 leaks) (#801)

Post-LGTM cleanup after Chi's re-review on PR #794:

- **#4 — renderGoal reorder** (tools.ts): goal-required guard now runs
  BEFORE renderGoal, so we never produce a string with an un-substituted
  `{goal}` placeholder. Today's effect is harmless (the unfilled string
  was unused), but the wasted call is the kind of bug that lands a
  prompt-injected `{goal}` in a future code path.

- **Leak 1 — tools.ts error hint** mentioned `vision_query / take_note /
  look_up_reference` as fallback behavior. Those tools don't exist yet
  (tracked in issue #797). Replaced with a neutral "operate normally
  with whatever tools the session already has registered."

- **Leak 2 — SKILL.md Privacy section** said transcripts get persisted
  "via the `take_note` tool" — also a forward reference to an unbuilt
  tool. Updated to acknowledge the tool is planned (#797) and that
  today "remember this" requests are persisted as part of the
  conversation transcript.

Chi's verdict was LGTM with these as cleanup-not-blockers — landing them
in the same PR before merge anyway.
qingyun-wu added a commit that referenced this pull request May 18, 2026
closes #800)

The "accepts all 5 valid agent states" test was alternating pass/fail in
CI (observed on PR #794 — runs 26007138651 and 26007377568 failed while
adjacent runs 26006469429, 26007215853, and earlier all passed, with
none of the commits touching agent-state plumbing).

Root cause: the browser-track loop (line 117) doesn't pre-clear the
tool track. If prior process state (or a prior test ordering) left the
tool track at `working` or `seeing`, `/sse-status` returns that
tool-track value because tool track has precedence by design. The
assertion `/sse-status should reflect listening` fails with `expected
'listening' got 'working'`.

The "tool track takes precedence" test at line 149 already pre-clears,
confirming the convention. Add the matching one-liner at the top of
line 117's test.

Verified locally: 3/3 clean runs. tsc clean.
qingyun-wu added a commit that referenced this pull request May 18, 2026
closes #800)

The "accepts all 5 valid agent states" test was alternating pass/fail in
CI (observed on PR #794 — runs 26007138651 and 26007377568 failed while
adjacent runs 26006469429, 26007215853, and earlier all passed, with
none of the commits touching agent-state plumbing).

Root cause: the browser-track loop (line 117) doesn't pre-clear the
tool track. If prior process state (or a prior test ordering) left the
tool track at `working` or `seeing`, `/sse-status` returns that
tool-track value because tool track has precedence by design. The
assertion `/sse-status should reflect listening` fails with `expected
'listening' got 'working'`.

The "tool track takes precedence" test at line 149 already pre-clears,
confirming the convention. Add the matching one-liner at the top of
line 117's test.

Verified locally: 3/3 clean runs. tsc clean.
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…nichi#794)

* feat(screen-companion): skeleton skill (M1) + pair-read-paper config

v0 skeleton for the screen-companion skill. Closes nothing yet — this
PR ships the *primitive*, not the working feature. Followup PR wires
the loaded config into voice-agent.ts.

What's in this PR:

- skills/screen-companion/SKILL.md — describes the skill, the M1 model
  (master skill + N declarative configs), how to add a new use case,
  trust + scope boundary.

- skills/screen-companion/configs/pair-read-paper.yaml — the first
  use-case config. Demonstrates the full schema: activation triggers
  (voice phrases + button label + CLI alias), vision_mode + cadence,
  system_prompt_overlay with domain priors + don't-list, tools_allow
  whitelist, goal_template for optional activation-time fill-in.

- skills/screen-companion/scripts/activate.ts — loader + activation
  printer. v0 scope: discover configs in configs/*.yaml, validate
  required fields, load one by name, print what *would* happen when
  it's activated. No voice-agent wiring yet — intentional, so the
  config schema can crystallize against a real concrete config
  (pair-read-paper) before the integration shape is committed.

CLI:
  npx tsx skills/screen-companion/scripts/activate.ts --list
  npx tsx skills/screen-companion/scripts/activate.ts --config pair-read-paper
  npx tsx skills/screen-companion/scripts/activate.ts --config pair-read-paper --goal "..."

Picks M1 ("master skill + declarative configs") from screen_companion.md.
Discussion + tradeoffs vs M2 (per-use-case sub-skills) + M3 (SDK) live
in that doc.

YAML parsing via python3 subprocess (same pattern as src/oc-profile-catalog.ts);
avoids adding js-yaml as an npm dep.

Next PR (followup):
- Wire system_prompt_overlay onto the active VoiceSession.
- Restrict the tool surface to tools_allow at activation time.
- Hook vision push/pull cadence.
- Add pair-debug + pair-review-code configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screen-companion): swap v0 config from pair-read-paper to guided-setup

Owner-picked 2026-05-17: the v0 first-shipped config is `guided-setup`
(proactive + goal-directed — Sutando narrates next steps for a UI
config task), not `pair-read-paper`. More visceral demo angle: "Sutando
helps me set up a Discord bot I've never configured before" vs. "Sutando
reads a paper with me."

Changes in this commit:
- Add skills/screen-companion/configs/guided-setup.yaml — the new v0
  config. PROACTIVE shape: tighter vision cadence (700ms vs 1000ms),
  prompt overlay narrates next steps continuously, explicit DO NOT
  click/type guidance (Sutando narrates; user keeps the action).
- Drop skills/screen-companion/configs/pair-read-paper.yaml — was
  shipping as the v0 reference config but the demo angle is weaker;
  owner-clarified the framing should center on "help me set up
  something I've never done before."
- Update SKILL.md table + run example + roadmap to reflect guided-setup
  as the shipped config; pair-read-paper dropped (siblings pair-debug
  + pair-review-code still tracked in screen_companion.md as future).
- Update activate.ts doc header + CLI example to use guided-setup.
- Add Privacy + data handling section to SKILL.md: default DOES NOT
  take screenshots, record video, or persist frames; frames are sent
  to Gemini Live for real-time understanding and discarded; text
  transcripts are persisted (conversation-server precedent); user
  has to explicitly opt-in per request to save screenshots/recordings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(screen-companion): wire guided-setup end-to-end via skill manifest

Closes the wiring gap left by the v0 skeleton — voice can now activate
guided-setup mode directly. Earlier passes shipped the YAML config + CLI
loader; this pass adds the missing piece:

- manifest.json — skill is enabled, points to tools.ts
- tools.ts — exports activate_screen_companion tool (loads config, returns
  prompt overlay + activation_message + vision hint as a structured payload)
- scripts/load-config.ts — shared loader used by both the CLI activate.ts
  and the inline tool (was duplicated; now single source of truth)
- scripts/activate.ts — refactored to import from load-config.ts (-80 lines)
- SKILL.md — drops the "v0 skeleton — load + print only" caveat, adds the
  voice activation phrases + the wire-through explanation

Smoke test (matches what voice will see):
  npx tsx -e "import('./skills/screen-companion/tools.ts').then(...)"
  → status: 'activated', mode: 'guided-setup', goal/instructions/tools_allow
    all populated, activation_message ready to speak.

Architecture: tool lives in the skill (per CLAUDE.md decision guide —
self-contained feature). Voice-agent picks it up via the existing skill
manifest loader (same pattern as gws-gmail-voice). No changes to
src/inline-tools.ts or src/voice-agent.ts.

Voice-agent restart required to register the new tool. Configs themselves
are read fresh per activation, so adding more configs in later PRs does
not require another restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screen-companion): tolerate LaunchAgent python3 without PyYAML

Voice-agent runs under a LaunchAgent whose PATH is
`/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin`. Homebrew's python3 wins
that lookup but does not ship PyYAML, so `python3 -c 'import yaml; ...'`
in parseYaml() failed every activation with ModuleNotFoundError. The
tool returned an error and Gemini fell back to default behavior — silent
to the owner aside from "screen companion failed."

Try a list of python3 candidates (system → homebrew → PATH default), use
the first that can run the yaml import. macOS's `/usr/bin/python3` ships
PyYAML, so the system Python is the usual winner under the LaunchAgent
PATH. Aggregate per-binary errors into the thrown message for diagnosis
when all candidates miss.

Verified under the LaunchAgent's PATH env: activation now succeeds.

* feat(vision): single-line nudge for screen-companion at share-start

The existing screen-share-started system note tells the model to say "I
can see your screen now" but stops there — so users who don't already
know the activation phrases (e.g. "help me set this up" for
guided-setup) share their screen and get no signal that pre-built
companion modes exist. Add a one-line, mode-agnostic nudge: "Want me
to help you through something?" If they describe a goal that fits any
configured mode, the model routes via `activate_screen_companion`.

Mode-agnostic on purpose — the prompt does not list configs by name, so
it won't age as new ones land (pair-debug, pair-review-code planned per
screen_companion.md). The model already knows the catalog from the
`activate_screen_companion` tool's description (which discovers configs
at boot), so listing them in the nudge would be redundant.

* feat(vision): share-start nudge names screen-companion + configs explicitly

First attempt (626a18f) shipped a mode-agnostic nudge ("Want me to help
you through something?") that did not surface the screen-companion
catalog. Owner test 2026-05-17 17:28 PT: routing worked (goal → mode)
but discoverability didn't — owner reported "I do not hear the nudge
about screen companion mode."

Updated injected prompt instructs Gemini to deliver ONE sentence that
(a) acknowledges the screen, (b) names "screen-companion mode"
explicitly, (c) names the available configs by name (Gemini reads them
from the activate_screen_companion tool description, which is
discoverConfigs() at boot — so new YAMLs surface automatically), (d)
asks the user's goal.

Example shape: "I can see your screen — screen-companion mode is
available with guided-setup for UI walkthroughs. What are you trying
to do?"

* fix(screen-companion): force ground-then-step in guided-setup overlay

Owner test 2026-05-17 17:34 PT: in a FRESH voice session with no
chat history, Gemini activated guided-setup for "set up a Discord bot"
and asserted "you're on the Discord Developer Portal" + "I see your
cursor hovering over those existing apps" — when the user was
demonstrably NOT on the dev portal. Goal-priming overrode actual
vision input. (Verified via fresh-session diagnostic — chat-history
anchoring was ruled out.)

Add a GROUNDING RULE section at the top of the overlay:
- Name what you ACTUALLY see (page title/URL/breadcrumb, dominant UI
  elements, overall page kind) BEFORE naming any step.
- If the visible page doesn't match the goal's destination, do NOT
  assert the user is on the right page — say what you see and ask them
  to navigate.
- Never fabricate UI elements; if a label is illegible, ask the user
  to read it.
- Goal text is a hint about WHERE the user is going, NOT proof of
  where they ARE. Reconcile goal vs visible-screen as two separate
  facts every turn.

Configs are loaded fresh on each activation — next re-activation picks
this up without a voice-agent restart.

* docs(screen-companion): downgrade tools_allow claim from 'enforced' to advisory

sonichi cold-review on PR sonichi#794 flagged: SKILL.md asserts the tool allow-
list is "enforced at activation time; configs cannot grant tools the
active VoiceSession doesn't already expose" — but tools.ts:67 just
returns the field in the activation payload as a string list. The
_note instructs Gemini to self-restrict; there is no path in the
bodhi-realtime-agent layer that scopes the live tool surface.

Update the Trust + scope section to match reality: advisory in v0,
hard enforcement deferred to a follow-up PR. The risk surface
("worst case is Gemini calls a tool you didn't intend") is named
explicitly so readers can calibrate.

Code unchanged — the field still functions as the documented intent
and Gemini does self-restrict in observed sessions; only the doc claim
was overstating.

* chore(screen-companion): defensive validation + SKILL.md typo (sonichi sonichi#5, sonichi#6, sonichi#8)

From sonichi cold-review on PR sonichi#794 — uncontroversial nice-to-haves
that don't depend on the scope decisions for findings sonichi#1–sonichi#3.

- sonichi#5: validateConfig now checks tools_allow is a string array. A YAML
  with `tools_allow: "vision_query"` (string instead of list) used to
  pass validation and crash later at `.length` access with a confusing
  error. Now fails fast at load with a clear message.
- sonichi#6: vision_cadence_ms bounded to 100–5000ms. A typo like 70000 (70s)
  would silently produce useless frame cadence; 50ms would saturate the
  network. The 100–5000 range covers every plausible mode today.
- sonichi#8: SKILL.md typo — "bash scripts/activate.ts --list" → "npx tsx ..."
  (matches the smoke-test block above; bash can't execute the .ts).

No behavior change for the live guided-setup config (verified by
--list and tsc clean).

* docs(screen-companion): mark guided-setup tools_allow as PLANNED (sonichi sonichi#1)

PR sonichi#794 review finding sonichi#1 — tools_allow lists three tools that don't
exist anywhere in the codebase (zero grep matches outside this skill).
sonichi's three suggested fixes were: rename to real tools, land the
missing tools, or document as schema-only. Going with the documentation
path for v0 — least-risky of the three, reversible, and consistent
with the truthfulness fix in eff1286 that already downgrades the
allow-list claim in SKILL.md from "enforced" to "advisory".

guided-setup as shipped relies only on push-mode vision frames + Gemini
Live's conversational capability — no tool calls are actually required
to operate. The PLANNED comment captures design intent without
fabricating wiring that doesn't exist; a follow-up PR registers real
tools and Gemini gets a real allow-list to follow.

Code unchanged. CI green expected — pure YAML comments.

* refactor(vision): contributor registry — remove screen-companion knowledge from core (sonichi sonichi#3)

PR sonichi#794 review finding sonichi#3: src/vision-tools.ts:254-256 hardcoded the
strings "screen-companion mode" and "guided-setup" in the screen-share-
started system note. Per CLAUDE.md decision guide, core services must
not contain feature-specific logic; if a user removed the screen-
companion skill, vision-tools.ts would lie to Gemini about modes that
no longer exist.

Refactor:

- src/vision-tools.ts now exposes `registerVisionOnContributor(fn)`.
  When push mode starts, the injected note is built from a generic base
  note + each contributor's output. With zero contributors registered,
  the note is generic and mentions no modes.
- skills/screen-companion/tools.ts registers a contributor at module-
  load time. The contributor calls discoverConfigs() at injection time,
  so new YAMLs in configs/ surface automatically without code changes.
- If the skill is disabled (manifest.enabled=false) or removed, the
  skill-loader at src/inline-tools.ts:937 never imports tools.ts, the
  contributor never registers, and vision-tools.ts emits only the base
  note — exactly matches CLAUDE.md "skills are optional."

Behavioral equivalence: with screen-companion enabled (default), the
injected note now contains the same essential content as before — just
sourced from the contributor instead of hardcoded.

Verified: tsc clean. Smoke test confirms contributor count goes 0→1
when the skill module is imported. Voice-agent restarted to pick up
the new module.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…iew sonichi#4 + 2 leaks) (sonichi#801)

Post-LGTM cleanup after Chi's re-review on PR sonichi#794:

- **sonichi#4 — renderGoal reorder** (tools.ts): goal-required guard now runs
  BEFORE renderGoal, so we never produce a string with an un-substituted
  `{goal}` placeholder. Today's effect is harmless (the unfilled string
  was unused), but the wasted call is the kind of bug that lands a
  prompt-injected `{goal}` in a future code path.

- **Leak 1 — tools.ts error hint** mentioned `vision_query / take_note /
  look_up_reference` as fallback behavior. Those tools don't exist yet
  (tracked in issue sonichi#797). Replaced with a neutral "operate normally
  with whatever tools the session already has registered."

- **Leak 2 — SKILL.md Privacy section** said transcripts get persisted
  "via the `take_note` tool" — also a forward reference to an unbuilt
  tool. Updated to acknowledge the tool is planned (sonichi#797) and that
  today "remember this" requests are persisted as part of the
  conversation transcript.

Chi's verdict was LGTM with these as cleanup-not-blockers — landing them
in the same PR before merge anyway.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants