From 67d4ddf7e7bc36f35aeb37fd33db677ed6705a8b Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 15 Oct 2026 19:57:27 +0200 Subject: [PATCH 1/3] fix(question, plugins): the native question tool is for real blockers only, and a post with the widget off is stored for later while the agent asks in its reply as well, both ways every time (cherry picked from commit 0f12713346c3497daf9cb855765b0bf3b47b53f7) --- .../genesis-tools/hooks/native-inbox-hint.ts | 2 +- .../lib/tools/question-answer.test.ts | 3 +- .../lib/tools/question-post.test.ts | 5 +- src/question/lib/inbox-guidance.ts | 48 ++++++++++++------- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/plugins/genesis-tools/hooks/native-inbox-hint.ts b/plugins/genesis-tools/hooks/native-inbox-hint.ts index 316ec3173c..bc1f7c5a71 100644 --- a/plugins/genesis-tools/hooks/native-inbox-hint.ts +++ b/plugins/genesis-tools/hooks/native-inbox-hint.ts @@ -27,7 +27,7 @@ export const RUNNING_HINT = 'INBOX: the user\'s GenesisTools widget is running on this Mac, and its inbox reaches the user even when they are not watching this chat. Post there only when you need the user: a decision you cannot make yourself, as a question_post item with type "decision" (CLI: `tools question ask --json -`); or a finished long task, a blocker, or a result they must see (attach the screenshot), as inbox_send with the screenshot paths in `images` (CLI: `tools question message "" --image /abs/shot.png`). Never post routine progress or anything you can decide yourself. Also write every question in your chat reply.'; export const INSTALLED_HINT = - 'INBOX: the GenesisTools app is installed on this Mac, but its widget is not running, so nobody may see the inbox. You may still post a decision, as a question_post item with type "decision" (CLI: `tools question ask --json -`), or a message or screenshot, as inbox_send with the screenshot paths in `images` (CLI: `tools question message "" --image /abs/shot.png`). But ALWAYS also ask the user directly in this chat, with your native question tool or in your reply.'; + 'INBOX: the GenesisTools app is installed on this Mac, but its widget is not running. A post is still stored and reaches the user when they turn the widget on, so keep posting: a decision as a question_post item with type "decision" (CLI: `tools question ask --json -`), a message or screenshot as inbox_send with the screenshot paths in `images` (CLI: `tools question message "" --image /abs/shot.png`). Until the widget is on, the user sees only this chat, so ALWAYS also write each question in your reply, as it is.'; export interface InboxStateRecord { installed?: unknown; diff --git a/src/genesis-tools-mcp/lib/tools/question-answer.test.ts b/src/genesis-tools-mcp/lib/tools/question-answer.test.ts index 8503eb46c5..7da2a04c0a 100644 --- a/src/genesis-tools-mcp/lib/tools/question-answer.test.ts +++ b/src/genesis-tools-mcp/lib/tools/question-answer.test.ts @@ -180,7 +180,8 @@ describe("inbox_send", () => { ); expect(receipt.context).toMatchObject({ agent: "codex", sessionId: "codex-thread" }); - expect(receipt.summary).toContain("also tell the user in this chat"); + expect(receipt.summary).toContain("Tell the user in this chat now as well"); + expect(receipt.summary).toContain("keep sending both ways"); }); it("refuses a message with neither text nor image, and an unknown session gets the warning", async () => { diff --git a/src/genesis-tools-mcp/lib/tools/question-post.test.ts b/src/genesis-tools-mcp/lib/tools/question-post.test.ts index 7e8c75e012..7db56724d1 100644 --- a/src/genesis-tools-mcp/lib/tools/question-post.test.ts +++ b/src/genesis-tools-mcp/lib/tools/question-post.test.ts @@ -504,7 +504,7 @@ describe("the opt-in nudge", () => { const [instructions, description] = texts(askViaQuestionTool, "installed"); expect(instructions).toContain(inboxInstructions("installed")); - expect(instructions).toContain("ALWAYS also ask the user directly in this chat"); + expect(instructions).toContain("ALWAYS also write each question in your reply"); expect(description).toContain("widget is not running"); expect(description).not.toContain("Post every ❓ DECISION"); @@ -512,7 +512,8 @@ describe("the opt-in nudge", () => { { sessionHint: "sess-1", items: [decision] }, withLog(askViaQuestionTool, "installed") ); - expect(note).toContain("Ask the user directly in this chat now"); + expect(note).toContain("Write the question in your chat reply now"); + expect(note).toContain("only for a real blocker"); } }); diff --git a/src/question/lib/inbox-guidance.ts b/src/question/lib/inbox-guidance.ts index 117612b349..58598ff0e8 100644 --- a/src/question/lib/inbox-guidance.ts +++ b/src/question/lib/inbox-guidance.ts @@ -26,6 +26,14 @@ export interface InboxGuidance { } const DECISION_DOOR = `a question_post item with type "decision" (CLI: \`${toolCommand("question ask", "--json", "-")}\`)`; + +/** + * When the native question tool is right. It ends the agent's turn until the user answers, so it is for a real + * blocker only (Martin, 2026-10-15): with other work unblocked, the question goes in the reply and the work goes on. + */ +const NATIVE_ASK = + "Use your native question tool (for example AskUserQuestion) only for a real blocker: it stops your work " + + "until the user answers. While other work is unblocked, write the question in your reply and keep working."; const MESSAGE_DOOR = `inbox_send with the screenshot paths in \`images\` (CLI: \`${toolCommand("question message")} "" --image /abs/shot.png\`)`; /** The INBOX paragraph: when to post and how. Empty without the native app. */ @@ -42,10 +50,10 @@ export function inboxInstructions(state: NativeInboxState): string { if (state === "installed") { return ( - "INBOX: the GenesisTools app is installed on this Mac, but its widget is not running, so nobody may see " + - `the inbox. You may still post a decision, as ${DECISION_DOOR}, or a message or screenshot, as ` + - `${MESSAGE_DOOR}. But ALWAYS also ask the user directly in this chat, with your native question tool ` + - "or in your reply." + "INBOX: the GenesisTools app is installed on this Mac, but its widget is not running. A post is still " + + "stored and reaches the user when they turn the widget on, so keep posting: a decision as " + + `${DECISION_DOOR}, a message or screenshot as ${MESSAGE_DOOR}. Until the widget is on, the user sees ` + + "only this chat, so ALWAYS also write each question in your reply, as it is." ); } @@ -56,8 +64,9 @@ export function inboxInstructions(state: NativeInboxState): string { export function decisionNudge({ state, askViaQuestionTool }: InboxGuidance): string { if (state === "installed") { return ( - "The GenesisTools widget is not running, so a post may go unseen: ask decisive questions with your " + - "native question tool (for example AskUserQuestion) or in your reply, and post here only as a copy. " + "The GenesisTools widget is not running: a post is stored and shows when the user turns it on, but " + + "until then the user sees only your reply. Post each ❓ DECISION here AND write it in your reply. " + + `${NATIVE_ASK} ` ); } @@ -65,14 +74,14 @@ export function decisionNudge({ state, askViaQuestionTool }: InboxGuidance): str return askViaQuestionTool ? "Post every ❓ DECISION this way instead of your native question tool: the user answers it in the " + "widget inbox. Also write it in your reply. " - : "Ask decisive questions with your native question tool (for example AskUserQuestion) and in your " + - "reply, and also post each ❓ DECISION here so it reaches the user's widget inbox. "; + : "Write each ❓ DECISION in your reply, and also post each ❓ DECISION here so it reaches the user's " + + `widget inbox. ${NATIVE_ASK} `; } return askViaQuestionTool ? "Post every ❓ DECISION this way, and also write it in your reply: the log is a copy. " - : "The user has NOT opted in to agents asking through question_post, so ask decisive questions with your " + - "native question tool (for example AskUserQuestion) and in your reply. "; + : "The user has NOT opted in to agents asking through question_post, so write decisive questions in your " + + `reply. ${NATIVE_ASK} `; } const REPLY_COPY = @@ -82,16 +91,16 @@ const REPLY_COPY = export function postNote({ state, askViaQuestionTool }: InboxGuidance): string { if (state === "installed") { return ( - "Saved to the inbox, but the GenesisTools widget is not running, so the user may not see it. Ask the " + - "user directly in this chat now: your native question tool (for example AskUserQuestion) or your reply." + "Stored in the inbox, but the GenesisTools widget is off, so the user sees it only once they turn it on. " + + "Write the question in your chat reply now, as it is, and keep posting each one here as well: both " + + `ways, every time. ${NATIVE_ASK}` ); } if (state === "running") { return askViaQuestionTool ? `Posted to the user's widget inbox. ${REPLY_COPY}` - : `Posted to the user's widget inbox. ${REPLY_COPY} Ask decisive questions with your native question ` + - "tool (for example AskUserQuestion) as usual."; + : `Posted to the user's widget inbox. ${REPLY_COPY} ${NATIVE_ASK}`; } if (askViaQuestionTool) { @@ -100,8 +109,7 @@ export function postNote({ state, askViaQuestionTool }: InboxGuidance): string { return ( `The user has not opted in to agents asking through ${toolCommand("question")} (${toolCommand("question config")}). ` + - "The post was saved, but ask decisive questions with your native question tool (for example " + - `AskUserQuestion) and in your chat reply. ${REPLY_COPY}` + `The post was saved. ${REPLY_COPY} ${NATIVE_ASK}` ); } @@ -112,7 +120,10 @@ export function messageNote(state: NativeInboxState): string { } if (state === "installed") { - return "Saved to the inbox, but the GenesisTools widget is not running: also tell the user in this chat."; + return ( + "Stored in the inbox; the GenesisTools widget is off, so the user sees it once they turn it on. Tell " + + "the user in this chat now as well, and keep sending both ways." + ); } return "Saved to the question log. This Mac has no native GenesisTools inbox, so tell the user in this chat."; @@ -123,7 +134,8 @@ export function inboxSendDescription(state: NativeInboxState): string { const when = state === "running" ? "The user's widget is running, so this reaches them even when they are not watching the chat." - : "The widget is not running, so nobody may see it: ALSO tell the user in this chat."; + : "The widget is not running: the message is stored and shows when the user turns it on, so ALSO tell " + + "the user in this chat, and keep sending both ways."; return ( "Send the user a message, optionally with screenshots, to their GenesisTools widget inbox. It lands as an " + From 69966076c2ef48cbb290f6d755ae30355dcf6395 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 16 Oct 2026 19:57:19 +0200 Subject: [PATCH 2/3] chore(plugins): genesis-tools 1.0.107 for the inbox guidance wording, above every open PR's version (squashes the bumps cc410679b, d567f11cb and 17bfe846e from the unified branch; the artifact kit they named ships in #531) --- .claude-plugin/marketplace.json | 4 ++-- plugins/genesis-tools/.claude-plugin/plugin.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 866db0007c..a699039b86 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "genesis-tools", - "version": "1.0.102", + "version": "1.0.107", "description": "Plugins for GenesisTools CLI development and management", "owner": { "name": "genesiscz", @@ -11,7 +11,7 @@ { "name": "genesis-tools", "description": "Skills and utilities for working with GenesisTools CLI toolkit. Provides guidance for discovering, executing, and troubleshooting genesis tools with integrated workflow support.", - "version": "1.0.102", + "version": "1.0.107", "author": { "name": "GenesisTools" }, diff --git a/plugins/genesis-tools/.claude-plugin/plugin.json b/plugins/genesis-tools/.claude-plugin/plugin.json index 4575379de4..dcd975f2fb 100644 --- a/plugins/genesis-tools/.claude-plugin/plugin.json +++ b/plugins/genesis-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genesis-tools", - "version": "1.0.102", + "version": "1.0.107", "description": "Skills and utilities for working with GenesisTools CLI toolkit. Provides guidance for discovering, executing, and troubleshooting genesis tools with integrated workflow support.", "author": { "name": "GenesisTools" From d0e28972e61c6497dbf7acc66b693ad059f38aa1 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 17 Oct 2026 01:39:51 +0200 Subject: [PATCH 3/3] fix(plugins): address PR review (2 commits) * fix(plugins): #535 followup - the research skill offers an optional install or a vault path save in its reply, because a native question stopped the run for a choice that is not a blocker * fix(plugins): #535 followup - the research skill's comparison example dispatches at once and offers the install in its reply --- .../genesis-tools/skills/research/SKILL.md | 21 +++++++++---------- .../skills/research/references/mcps.md | 7 ++----- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/plugins/genesis-tools/skills/research/SKILL.md b/plugins/genesis-tools/skills/research/SKILL.md index 8ce9ff4ffe..fe4f270828 100644 --- a/plugins/genesis-tools/skills/research/SKILL.md +++ b/plugins/genesis-tools/skills/research/SKILL.md @@ -65,19 +65,18 @@ Always record what was missing and what substitution was used in `Confidence & G ### Offer install (at most once per run) -If a missing MCP would materially improve the run AND the user didn't say "quick"/"temporary"/"just tell me", ask ONCE before dispatch (`AskUserQuestion`, `request_user_input_async` or `ask_user_question`, whichever your harness has): +If a missing MCP would materially improve the run AND the user didn't say "quick"/"temporary"/"just tell me", offer it ONCE, and dispatch with the degraded toolset without waiting: an install cannot help THIS run anyway (step 3 below). Write the offer in your reply, not in a native question tool (`AskUserQuestion` stops the run until the user answers, and this is not a blocker): -- **Question:** "I'd dispatch this with `` for ``, but it's not installed. Want install instructions, or proceed without?" -- **Options:** `Install now`, `Skip and proceed`, `Don't ask again this run`. +- **Offer:** "I dispatched this without ``, which would add ``. Say "install" for the install steps." -If the user picks **Install now**, read `references/mcps.md` from this skill directory and output: +If the user answers **install**, read `references/mcps.md` from this skill directory and output: 1. The `bun add --global ` command (if local) or "no install — hosted HTTP MCP" (if remote). 2. The exact `mcpServers` JSON snippet for the user to paste into their Claude config (or the equivalent `claude mcp add` one-liner). 3. Note that the user must restart their agent for the new server to become callable (Claude Code and Codex reload MCP config on restart; Grok re-reads `~/.grok/config.toml` on start too), and the skill will proceed without it for THIS run. The skill does **not** execute install commands itself. The user runs the install. The skill then proceeds with whatever is currently available. -If the user picks **Skip** or **Don't ask again**, dispatch immediately with the degraded toolset. +Do not repeat the offer for another missing MCP in the same run. ## Categories (6) @@ -314,10 +313,10 @@ Resolve the vault path in this order; **stop at the first hit:** ### Persist the resolved path (offer once) -If the path came from steps 2–5 (i.e. it was **not** already in `$OBSIDIAN_VAULT_PATH`) — and this isn't a "temporary"/"quick" run — offer once to persist it so future runs skip detection. **Exception:** if the path came from step 2 (a user-named path), only offer to persist it when it's actually a vault root (contains a `.obsidian/` subdir); a one-off subfolder like `~/Notes/scratch` should be used for this run but never saved as the global `OBSIDIAN_VAULT_PATH`. `AskUserQuestion`: +If the path came from steps 2–5 (i.e. it was **not** already in `$OBSIDIAN_VAULT_PATH`) — and this isn't a "temporary"/"quick" run — offer once to persist it so future runs skip detection. **Exception:** if the path came from step 2 (a user-named path), only offer to persist it when it's actually a vault root (contains a `.obsidian/` subdir); a one-off subfolder like `~/Notes/scratch` should be used for this run but never saved as the global `OBSIDIAN_VAULT_PATH`. Write the offer in your reply and keep working with the resolved path; persist only when the user answers. It is not a blocker, so not a native question tool: -- **Question:** "Save `OBSIDIAN_VAULT_PATH=` so I can skip detection next time?" -- **Options:** +- **Offer:** "Save `OBSIDIAN_VAULT_PATH=` so I can skip detection next time?" +- **Choices:** - **`~/.claude/settings.json`** — Claude-only; the harness injects the `env` block into Claude Code's environment at session start. Add/replace `OBSIDIAN_VAULT_PATH` inside the top-level `env` object (create `env` if absent). **Use the Read + Edit tools to splice the one key in place** — do NOT parse-and-rewrite the whole file (settings.json is JSONC; a strict `JSON.parse`/stringify round-trip would strip comments and reformat unrelated keys). - **Shell rc** — all tools, all shells. Detect from `$SHELL`: zsh → `~/.zshrc`, bash → `~/.bashrc`, fish → `~/.config/fish/config.fish`. Append `export OBSIDIAN_VAULT_PATH=""` (fish: `set -Ux OBSIDIAN_VAULT_PATH ""`). If a line already sets it, replace that line instead of duplicating. - **`Just this run`** — don't persist. @@ -482,9 +481,9 @@ SAVE RAW REPORT TO: - **Source minimums are hard** (see table above). No invention, no padding. - **Always run `scripts/resolve.ts` when saving, and surface its `warnings`.** Never hardcode `.claude/work/research/`, never read the config file yourself, and never fall back to `defaultPath` when a project declares a resolver that failed. - **Ask the default path at most once until remembered.** If `config.json` is missing or unusable and this run will save, ask then write the file. Do not re-ask on later runs while the config is valid. "Change research path" is the explicit re-ask trigger. -- **`AskUserQuestion` budget.** Up to **three** for the core run: once for the optional install offer, once for ambiguity/depth, once for the default research path when config is unset. **Plus up to two more** — *only* when an Obsidian path is required and `$OBSIDIAN_VAULT_PATH` isn't already set: one for vault resolution (pick-from-list **or** free-text, never both) and one for the vault persist offer. Never prompt about the vault when no Obsidian path is needed. +- **`AskUserQuestion` budget.** Up to **two** for the core run: once for ambiguity/depth, once for the default research path when config is unset. **Plus one more**, *only* when an Obsidian path is required and `$OBSIDIAN_VAULT_PATH` isn't already set: vault resolution (pick-from-list **or** free-text, never both). The optional install offer and the vault persist offer are written in the reply and never wait for an answer. Never prompt about the vault when no Obsidian path is needed. - **Tool-availability fallback.** Missing MCP → drop from allowlist, substitute per the table, note in "Confidence & Gaps". -- **Install offer is opt-in**, never automatic. Default to Skip when in doubt. Never run install commands on behalf of the user — show the command and config snippet only. +- **Install offer is opt-in**, never automatic, and the run never waits for it. Leave it out when in doubt. Never run install commands on behalf of the user — show the command and config snippet only. - **Vault detection is gated on an Obsidian path being needed**, and persists only with explicit consent. Never auto-edit `settings.json` or a shell rc without the user picking that option in the persist offer. - **"Temporary" override.** Query contains "temporary", "just tell me", "don't save", "quick", "no need to save" → skip file write, skip install offer, skip default-path ask, skip the vault persist offer, inline only. Source minimums still enforced. - **Concurrency cap = 4 parallel agents** per research run. If a preset would spawn more, drop the lowest-priority angle. @@ -499,7 +498,7 @@ User: "what's the current stable version of Bun?" **Example 2 — comparison, first save (no research config yet), reddit MCP missing** User: "research state management options for React in 2026" -→ category=`comparison`, `needsSave=true`, `scripts/resolve.ts` answers `found: false` → ask once where research for this project belongs. User picks Project `.claude/work/research/`. Write the `research` key of the shared config (`pathKind: project-relative`, `defaultPath: .claude/work/research`). Detect `reddit-mcp-server` missing, ask once: install / skip / don't ask. User picks Install. Skill outputs `bun add --global reddit-mcp-server` and the config snippet from `references/mcps.md`. User restarts later. Skill proceeds with Sonnet docs + Haiku GitHub (Reddit angle dropped this run, gap recorded). 8 sources combined, save to `/.claude/work/research/2026-04-27-ReactStateManagement2026.md`. +→ category=`comparison`, `needsSave=true`, `scripts/resolve.ts` answers `found: false` → ask once where research for this project belongs. User picks Project `.claude/work/research/`. Write the `research` key of the shared config (`pathKind: project-relative`, `defaultPath: .claude/work/research`). Detect `reddit-mcp-server` missing and dispatch at once with Sonnet docs + Haiku GitHub (Reddit angle dropped this run, gap recorded). The reply offers the install once, in text. The user answers "install" later, and the skill outputs `bun add --global reddit-mcp-server` and the config snippet from `references/mcps.md`; it takes effect after a restart. 8 sources combined, save to `/.claude/work/research/2026-04-27-ReactStateManagement2026.md`. **Example 3 — sentiment, user declined install** User: "what do people complain about with tRPC, quick" diff --git a/plugins/genesis-tools/skills/research/references/mcps.md b/plugins/genesis-tools/skills/research/references/mcps.md index 87a0371ae8..5fd2cb1743 100644 --- a/plugins/genesis-tools/skills/research/references/mcps.md +++ b/plugins/genesis-tools/skills/research/references/mcps.md @@ -11,11 +11,8 @@ Check whether an MCP is available in the current session by looking at the syste When the skill decides an MCP would materially improve a research run and it's missing, it MUST: 1. Tell the user **what the MCP adds** to this run, in one sentence ("brave-search would let me cross-check facts against a second search index"). -2. Offer install via `AskUserQuestion` with three choices: - - **Install now** (recommended) — show the install command and config snippet, then continue once they confirm install. - - **Skip** — proceed without it, note the gap in `Confidence & Gaps`. - - **Don't ask again this run** — proceed and don't re-prompt for any other missing MCP this run. -3. If the user picks Install now, output the **install command** AND the **config snippet** the user pastes into their Claude Code MCP config (the user runs the install themselves; the skill does not auto-run install commands). +2. Offer the install in the reply, once per run, and dispatch without it: an install takes effect only in a new session, so the run never waits for the answer. Not a native question tool such as `AskUserQuestion`: it stops the run, and an optional install is not a blocker. Note the gap in `Confidence & Gaps`. +3. If the user answers "install", output the **install command** AND the **config snippet** the user pastes into their Claude Code MCP config (the user runs the install themselves; the skill does not auto-run install commands). Default to **Skip** if the run is small, low-stakes, or the user typed "quick"/"just tell me"/"temporary".