The first task after setup connects the picks and finishes something small within minutes (#129646)

* feat(onboarding): the first task chat delivers a small result in minutes

Live runs on an RTX Spark showed the task chat surveying the whole machine,
asking a plan question, and installing the picked plugins 11.5 minutes after
setup. Its suggestions ignored the picks and the scan, and the handoff text
said "Call me Sid." and "Start from this; look at my computer again...".

- New optional skill first-task. start_chat appends it to the task chat's
  first message, after the ask and the "What setup learned" block, with a
  JSON block of the connector and plugin ids picked in setup. No system prompt
  changes. The history projection cuts the message at its marker, so no
  surface shows the skill. Its rules: connect and install the picks first,
  work visible in 1 minute and a finished result in 5, big asks cut to a
  first slice, three options only for a vague ask, machine setup as a short
  card interview.
- setup_choose records the picked ids and the fork pick. The fork card is
  filled when shown: two first tasks from the picks and the scan (a daily
  brief from the picked apps, "Install a few apps for this Spark", a small
  page), then "I have something in mind", "Help me set up this <machine>",
  "Let's figure it out together". The machine-first "Something else" step,
  "automate" and "skip" rows are gone; "figure" hands off at once.
- The handoff template is the ask only; no "Call me", no install or plan
  paragraphs. start_chat drops the "Start from this" line.
- initiate-setup SKILL.md keeps only the setup beats (20.5 KB -> 10 KB).
- Desktop shows the "What setup learned" block folded under the first
  message; the model still gets the full text.

* fix(setup): one unexpected shutdown reads as singular
This commit is contained in:
Siddharth Balyan
2026-10-01 02:26:02 +05:30
committed by GitHub
parent 2c018e8200
commit ff4760bfbd
18 changed files with 549 additions and 386 deletions

View File

@@ -0,0 +1,29 @@
"""The ``first-task`` skill for the task chat a setup handoff opens.
``start_chat`` sends it in that chat's first user message, the way ``/initiate-setup`` carries its skill, so
no system prompt changes. It rides at the tail, after the visible ask and the "What setup learned" block:
session previews read the ask, and the history projection cuts the message at ``MARKER`` so no surface
shows the skill.
"""
from __future__ import annotations
import json
from pathlib import Path
from hermes_constants import get_optional_skills_dir
MARKER = "\n\n[/first-task]\n\n"
def first_task_tail(connect: list, install: list) -> str:
"""``connect``: connector ids to show on one connect card; ``install``: catalog plugin ids to install."""
skill_dir = get_optional_skills_dir(Path(__file__).resolve().parent.parent / "optional-skills")
skill = (skill_dir / "productivity" / "first-task" / "SKILL.md").read_text(encoding="utf-8-sig").strip()
facts = json.dumps({"connect": connect, "install": install}, ensure_ascii=False)
return f"{MARKER}{skill}\n\n```json\n{facts}\n```"
def visible_text(text: str) -> str:
"""The part of a user message a surface shows: everything before the first-task skill."""
return text.partition(MARKER)[0]

View File

@@ -72,6 +72,11 @@ def collect_setup_cards() -> dict:
return host_facts.setup_cards(_collect(host_facts))
def fork_card(cards: dict) -> dict:
"""The fork card as it is shown, its first rows built from the picks this setup conversation recorded."""
return _host_facts_module(_skill_dir()).fork_card(cards)
def build_initiate_setup_prompt(surface: str, tools, primary_profile: str, session_id: str | None = None) -> str:
"""``session_id``: the desktop session the turn runs in; its ``setup_choose`` cards read these same facts."""
from hermes_cli.anon_auth import free_tier_route

View File

@@ -0,0 +1,23 @@
import type { FC } from 'react'
// tui_gateway/start_chat.py::_setup_learned: the block a setup handoff appends
// under the user's ask. The model reads all of it; the bubble shows only the ask.
const SETUP_LEARNED_MARKER = '\n\nWhat setup learned about me:\n'
/** `[ask, block]`: the block is null for every message that has none. */
export function splitSetupLearned(text: string): [string, null | string] {
const index = text.indexOf(SETUP_LEARNED_MARKER)
return index < 0 ? [text, null] : [text.slice(0, index), text.slice(index + 2)]
}
export const SetupLearnedNote: FC<{ text: string }> = ({ text }) => {
const [title = '', ...lines] = text.split('\n')
return (
<details className="mb-2 text-[0.75rem] text-muted-foreground" data-slot="aui_setup-learned">
<summary className="cursor-pointer select-none hover:text-foreground/70">{title.replace(/:$/, '')}</summary>
<div className="mt-1 whitespace-pre-line leading-5 text-foreground/75">{lines.join('\n')}</div>
</details>
)
}

View File

@@ -9,6 +9,7 @@ import {
PROCESS_NOTIFICATION_RE
} from '@/components/assistant-ui/thread/content'
import { ReactionBadge, ReactionPicker } from '@/components/assistant-ui/thread/message-reactions'
import { SetupLearnedNote, splitSetupLearned } from '@/components/assistant-ui/thread/setup-learned'
import { BackgroundResult } from '@/components/assistant-ui/thread/system-message'
import { threadUserOrdinal } from '@/components/assistant-ui/thread/thread-message-index'
import { MessageTimelineTimestamp } from '@/components/assistant-ui/thread/timeline-timestamp'
@@ -249,7 +250,8 @@ export const UserMessage: FC<{
const copy = t.assistant.thread
const messageId = useAuiState(s => s.message.id)
const content = useAuiState(s => s.message.content)
const messageText = messageContentText(content)
const fullText = messageContentText(content)
const [messageText, setupLearned] = splitSetupLearned(fullText)
const threadRunning = useAuiState(s => s.thread.isRunning)
const latestUserId = useAuiState(s => {
@@ -411,11 +413,14 @@ export const UserMessage: FC<{
// it. No negative margin: -mt-* pulls the row up into the sticky box,
// where the sticky-prompt clip hides its top even at rest. Image refs
// render as thumbnails, file refs as chips; no border.
attachmentRefs.length > 0 && !chipOnlyTurn ? (
<div className="mb-2 flex flex-wrap gap-1">
<DirectiveContent text={attachmentRefs.join(' ')} />
</div>
) : null
<>
{attachmentRefs.length > 0 && !chipOnlyTurn && (
<div className="mb-2 flex flex-wrap gap-1">
<DirectiveContent text={attachmentRefs.join(' ')} />
</div>
)}
{setupLearned && <SetupLearnedNote text={setupLearned} />}
</>
}
messageId={messageId}
>
@@ -527,7 +532,7 @@ export const UserMessage: FC<{
event.stopPropagation()
triggerHaptic('selection')
onRequestRestoreConfirm?.(messageId, {
text: messageText,
text: fullText,
userOrdinal: runtimeUserOrdinal
})
}}

View File

@@ -0,0 +1,114 @@
---
name: first-task
description: "Run the first task chat that setup hands off."
version: 0.1.0
author: "Siddharth Balyan (alt-glitch) + Hermes Agent"
license: MIT
platforms: [linux, macos, windows]
metadata:
hermes:
tags: [onboarding, first-run, desktop, handoff]
category: productivity
related_skills: [initiate-setup]
---
# First Task Skill
Runs the first chat after setup, the one `start_chat` opened. The top of this message is the user's ask, then "What setup learned about me" (their picks and the scan), then these rules and a JSON block. The goal: they see work within one minute and hold a finished, useful result within five.
## When to Use
- This message carries the skill under a handoff from setup.
- Until the first result lands. After that it is a normal chat.
## Prerequisites
The task chat's own tools: `manage_connections`, `manage_catalog`, `clarify`, `terminal`, the file tools, the browser, `tool_search`, `skill_view`, and `desktop_preview` in the desktop app. Use only tools that are in your tool list.
## How to Run
The JSON block at the end holds `connect`, the connector ids of the apps they picked in setup, and `install`, the catalog plugin ids they picked or their first task brings. The ids are exact: skip search and status checks.
The "What setup learned" lines are your recon. Do not survey the machine first: no system scan, no update search, no inventory of installed tools. Look at one thing only when the next step needs it.
## Quick Reference
| Step | What | Call |
|---|---|---|
| 1 | One line on what you start with | none |
| 2 | Connect the picked apps | `manage_connections` connect, every id in `connect`, once |
| 3 | Install the picked plugins | `manage_catalog` install, every id in `install`, once |
| 4 | Specific ask: start it. Vague ask: three options | `clarify`, three choices |
| 5 | The first slice, finished in five minutes | the task's own tools |
| 6 | Show the result, offer the next step | `clarify`: Looks right, Change something, Take it further |
```
manage_connections {"action":"connect","connectors":["<id>", ...]}
manage_catalog {"action":"install","items":[{"kind":"plugin","id":"<id>"}, ...],"reason":"<one line>"}
clarify {"questions":[{"question":"<short question>","choices":["<option>","<option>","<option>"]}]}
```
## Procedure
### 1. Connect first
Your first reply is one short line on what you will start with, then steps 2 and 3 back to back, before any other work. Skip a step whose list is empty. A skipped or failed row never blocks: say in one line what it would have added, go on without it, and never offer it again in this chat.
### 2. Ask only when vague
- A specific ask (a named outcome such as "A daily brief from Linear and Slack", "Install a few apps for this Spark", a scene in Blender): start it now, with no confirming question.
- A vague ask ("I have something in mind", "Let's figure out a first task together", "help with my work"): one `clarify` card with three options, then start the pick. Text they type instead is the pick.
Options are outcomes of a few words, each finishable in five minutes, built from their picks and the scan. Name only apps they picked or the scan saw in use.
- Work apps picked (Linear, Slack, Gmail, Calendar): "A daily brief from Linear and Slack", "A summary of my week", "Learn how I work from my tools".
- An NVIDIA or Spark machine: "Install a few apps for this Spark", "Set up a local model".
- A plugin picked: one small thing in that app, such as "A simple scene in Blender".
- Nothing picked: "A small HTML page about <something from the scan>", a quick useful script.
### 3. Time box
- Within one minute they see work: a file being written, a page opening, a first install.
- Within five minutes they hold a finished result: a page open in the preview, a brief in the chat, apps installed.
- A big ask gets cut to a first slice. Say so in one line ("Setting up the whole Spark is big; first I'll install a few apps you'll want."), finish that slice, then offer the next one.
- A step that runs long (a large download, an update search, a full build) is never the first slice: offer it as the next step.
- No written plan before work, except the machine interview below.
### 4. Machine setup is an interview
For "Help me set up this <machine>" or "Install a few apps":
1. Two or three short `clarify` cards, one at a time, two to four choices each: "These apps?" (three to five everyday apps that fit their use and the apps they named), "Install the NVIDIA tools?" (NVIDIA machines only), "A local model?" (a Spark or a strong GPU only).
2. Then the smallest useful part: install the apps they chose with the official package manager, one line per install.
3. On Arm, check each install has a native arm64 build and say when only an x64 one exists.
4. Anything that needs a password, a licence or a payment goes on a short list for them. Never disable security settings or overwrite config without asking.
Finish with what changed and one offer for the next slice (drivers, a local model, developer tools).
### 5. Build rules
- Real data only: from connected apps (find their tools with `tool_search`) or tools already signed in on this computer, such as a logged-in `gh` (say so in one line). Never mock or sample data. Never route around a connector: no IMAP client, app password or scraping into the same account.
- Ask before sending, deleting or scheduling anything. Set up no recurring job unless they asked.
- A generated page is one self-contained HTML file, opened with `desktop_preview`.
- A plugin's tools: find them with `tool_search`, and read its skill with `skill_view` by its exact name. If its app is not running, say so plainly.
- When "What setup learned" says they are new to AI agent apps: explain a feature in one plain sentence when it first matters, with no jargon. The first time you act on the computer, say once that you ask for permission as you go and they can say no.
### 6. Close the first slice
One short line on what you made and where it is, then a `clarify` card with "Looks right", "Change something" and "Take it further". Act on the pick.
## Pitfalls
- A survey of the machine before any work. The facts block is the survey.
- A plan question when the ask was already specific.
- A 30-minute job as the first slice.
- Offering again an app they skipped.
- Reading the facts block back to them.
- Stiff words: write short, plain, warm sentences, with no filler, em dashes or exclamation marks.
## Verification
- The first reply holds one line of text, then the connect and install calls.
- Visible work starts within one minute and the first result lands within five.
- A vague ask got one three-option card; a specific ask got none.
- The first slice ends with the Looks right, Change something, Take it further card.

View File

@@ -18,22 +18,22 @@ Sources mapped: `apps/desktop/src/store/onboarding-script.ts` (the runbook), `ap
| `::onboarding{step="layout"}` (LayoutCard: preset + interface mode + window grow) | `cards/setup.tsx::LayoutCard`, `assembly.ts::assembleChatOnboarding` | `setup_choose kind:"layout"`; `apply_layout` only for a layout asked for in words | Beat 3 |
| Step 4 model-picker explanation | runbook | Dropped; the tour's model picker stop covers it, in both presets | - |
| `::ask` "Want a look around first?" + `gui_tour` targets/start | runbook step 4 | `setup_choose kind:"tour"` (rows filled by the backend) + one `gui_tour` `start` with `preset` `quick` or `full` (the app's own tour) | Beat 4 |
| `::ask` fork, `input="true"` | runbook step 5, `forkOptions()` | `setup_choose kind:"fork"`; the backend fills `fork.options` computed by `scripts/host_facts.py` | Beat 5 |
| `::ask` "What sounds better?" (Something else) | runbook, `forkFallbackOptions()` | The same `fork` call: "Something else" opens `fork.fallback_options` in the backend | Beat 5 |
| `::ask` fork, `input="true"` | runbook step 5, `forkOptions()` | `setup_choose kind:"fork"`; the backend fills the rows when the card is shown: two first tasks from the apps and plugins picked earlier and the scan (`host_facts.fork_card`), then `mind`, `machine`, `figure` | Beat 5 |
| `::ask` "What sounds better?" (Something else) | runbook, `forkFallbackOptions()` | Dropped (2026-10-01): the first-task rows lead instead | - |
| Machine branch: one question on main use | runbook step 6 | `setup_choose kind:"machine_use"`; the backend fills Work / Gaming / School / Creative / A bit of everything | Beat 6 |
| `::onboarding{step="working" value}` (saves `answers.context`) | `directive.tsx` DATA_STEPS | Nothing. History + `start_chat` message part 2 | Beat 6, Beat 7 |
| `::onboarding{step="first" options}` (FirstBuildCard, 2-4 pills, each <= 60 chars, fallback pill) | `cards/build.tsx::FirstBuildCard` | `setup_choose kind:"question"` with 3-4 options suited to pills | Beat 6 |
| Install beat: one `manage_catalog` install batch in the setup chat | runbook `installBeat()` | Moved to the task chat (Sid, D4): handoff part 4 asks its first turn for ONE `manage_catalog` install call with only the plugins the task needs (all picked for machine setup or a task naming the app; `plugin_tasks` bring their own), then ONE `manage_connections` connect call | `templates/handoff.md` part 4 |
| `::onboarding{step="handoff" task brief plan}` + HandoffCard + `requestSetupHandoff` + hidden runbook seed in the new session | `cards/build.tsx::HandoffCard`, `setup-profile.ts` | `start_chat {message, title, profile: primary_profile}`; the plan runbooks become paragraphs of the visible message, in `templates/handoff.md`, which the fork result carries | Beat 7 |
| Install beat: one `manage_catalog` install batch in the setup chat | runbook `installBeat()` | Moved to the task chat (Sid, D4): the `first-task` skill asks its first turn for ONE `manage_connections` connect call and ONE `manage_catalog` install call with the ids `start_chat` hands it (the picks, plus a picked `plugin_tasks` row's plugins) | `first-task` skill |
| `::onboarding{step="handoff" task brief plan}` + HandoffCard + `requestSetupHandoff` + hidden runbook seed in the new session | `cards/build.tsx::HandoffCard`, `setup-profile.ts` | `start_chat {message, title, profile: primary_profile}`: the visible ask, then the folded "What setup learned" block and the `first-task` skill that `start_chat` appends (`agent/first_task_prompt.py`) | Beat 7 |
| `[setup] handoff complete` note | `setup-profile.ts::buildHandoffCompleteNote` | `start_chat` result `started` | Beat 8 |
| Handoff-failed note + "Retry first build" button | runbook step 8, HandoffCard | `start_chat` result `rejected`; the bot says so and retries once, with the same `profile`, when the user says yes | Beat 8 |
| `[setup] <summary>` hidden user rows after each card | `cards/frame.tsx` | `setup_choose` tool results | Tool rules |
| `::onboarding{step="progress" title}` in the task chat | `setup-profile.ts`, ProgressCard | Dropped (directives are deleted). No replacement (open question 9) | - |
| `::ask` "Does this match what you wanted?" in the task chat | `setup-profile.ts` | Handoff message part 6 asks the task chat to ask (it has `clarify`) | `templates/handoff.md` part 6 |
| `::ask` "Want me to run this?" (machine-setup) in the task chat | `MACHINE_SETUP_RUNBOOK` | Machine-setup paragraph of the handoff message | Beat 7 |
| `::ask` "Does this match what you wanted?" in the task chat | `setup-profile.ts` | The `first-task` skill closes the first slice with a `clarify` card | `first-task` skill |
| `::ask` "Want me to run this?" (machine-setup) in the task chat | `MACHINE_SETUP_RUNBOOK` | The `first-task` skill runs machine setup as a short `clarify` interview | `first-task` skill |
| `[setup] checkpoint` note after 8 and 20 tool calls in the task chat | `first-build.ts` | No carrier (open question 9) | - |
| Post-handoff tour of the profile rail and sessions list | `signpost.ts::showHandoffTour` | Beat 8 line says where the setup chat lives. The rail tour itself is not in the skill: after `start_chat` the user may already be in the new chat (open question 6) | Beat 8 |
| "Skip this for now" fork option | runbook step 6 | Fork id `skip` | Beat 6 |
| "Skip this for now" fork option | runbook step 6 | Dropped (2026-10-01); a cancelled fork still steps down the skip ladder | Beat 6 |
| Skip button (applies `basic` layout, marks skipped) | `assembly.ts::skipChatOnboarding` | Renderer / backend marker (NS-1016). The bot handles "I want to leave" in words | Failure handling |
## 2. Guidance to its new home
@@ -126,7 +126,7 @@ Directive parsing and `DATA_STEPS`/`STEP_CARDS`; `FUNNEL_STEPS` metrics (`record
- The whole setup can run inside one agent turn because `setup_choose` blocks.
- `start_chat` targets `primary_profile`, not always `default`, and a retry never drops it.
- A later `/initiate-setup` after a handoff (`setup_completed_at` set) skips to the fork for a new task.
- The first-task handoff carries its whole runbook as visible text.
- The task chat's rules travel as the `first-task` skill at the tail of its first message; the visible text is only the ask.
## 5. Host facts: renderer field to script field
@@ -162,5 +162,5 @@ Resolved by Sid (2026-09-30): skill location (stays in `optional-skills/`); the
11. **SOUL check-ins.** `SETUP_SOUL` tells the bot to look at sessions, connectors and scheduled jobs before checking in. The setup profile's tools cannot read those.
12. **One turn or many.** `setup_choose` blocks, so the whole setup can run in one agent turn. What happens on a card timeout, a closed window mid-card, or a relaunch with a pending card? The skill says: take the default and continue from the first unanswered beat.
13. **Other surfaces.** P13 runs `/initiate-setup` on CLI, TUI and messaging too, in the user's own profile with full tools. The skill falls back to plain-text questions, skips the tour without `gui_tour`, and starts the task in the same chat without `start_chat`. Should it use the extra tools those sessions have?
14. **Size.** `SKILL.md` is under 25 KB (~245 lines) after the 2026-09-30 trim; the handoff plan paragraphs are most of it.
14. **Size.** `SKILL.md` is under 10 KB after the 2026-10-01 split; every task-chat rule moved to the `first-task` skill.
15. **Host facts gaps.** Linux has no home birth time in `os.stat`, so a fresh Linux machine never leads with machine setup (Electron used `statx`). Name and locale are read in the skill script, not in `hermes_platform.host`, which covers hardware only. Should `hermes_platform.host` grow a birth-time helper and account facts?

View File

@@ -1,7 +1,7 @@
---
name: initiate-setup
description: "Run the first-run setup chat in the Hermes desktop app."
version: 0.2.0
version: 0.3.0
author: "Siddharth Balyan (alt-glitch) + Hermes Agent"
license: MIT
platforms: [linux, macos, windows]
@@ -9,71 +9,62 @@ metadata:
hermes:
tags: [onboarding, setup, first-run, desktop, handoff]
category: productivity
related_skills: [first-task]
---
# Initiate Setup Skill
Runs a new user's first conversation with Hermes. On desktop the app usually plays the opening (the welcome, the name card, the accent card), and you take over from the accent answer: the apps they use, plugins, layout, the tour offer, then one real first task, started in its own chat with `start_chat`. The facts below and their words shape all of it. You do not do the task, install anything, or read the machine.
Runs a new user's first conversation with Hermes. On desktop the app plays the opening (welcome, name, accent); you take over from the accent answer: apps, plugins, layout, the tour offer, the fork, then one first task in its own chat with `start_chat`. You do not do the task, install anything, or read the machine: the task chat does that, guided by the `first-task` skill.
## When to Use
- The `/initiate-setup` command started this turn.
- The user asks to run setup again from the setup chat.
Do not use it inside a task chat. When `setup_completed_at` is set, or `start_chat` already started a task from this chat, setup is done: never replay it. Say so in one line and go to beat 5 for a new task.
Never inside a task chat. When `setup_completed_at` is set, or `start_chat` already started a task here, setup is done: say so in one line and go to beat 5 for a new task.
## Prerequisites
The setup profile's tools on desktop (setup never uses `tool_search`, `tool_describe` or `tool_call`):
The setup profile's desktop tools:
- `setup_choose` shows one card and blocks until the user answers. `options` is always sent: at most 12 `{id, label, detail}`, or `[]` for the app's own list (free text for `question`); `tour`, `fork` and `machine_use` always show the app's rows. Returns `{outcome, picked, label, next, handoff}`: `picked` is an option id, a list of ids with `multi_select`, or the text they typed; `label` is the name the user saw for each pick, so name the pick by its `label`, never by its id; `next`, when present, is the flow's next step: do it in the same turn; the fork's result carries `handoff` for beat 7.
- `start_chat` starts a visible chat whose first user message is your `message`, in `profile`. Returns `{status: "started", session_id, profile, title}` or `{status: "rejected", reason}`. Each call starts one more chat.
- `apply_layout` applies a layout preset by id.
- `gui_tour` highlights parts of the app.
- `manage_connections` connect shows the app's sign-in card and blocks until the user finishes or skips it.
- `setup_choose` shows one card and blocks until the user answers. Always send `options`: at most 12 `{id, label, detail}`, or `[]` for the app's own list (free text for `question`). Returns `{outcome, picked, label, next, handoff}`: name a pick by its `label`, never its id; do what `next` says in the same turn; the fork's result carries `handoff` for beat 7.
- `start_chat` starts a visible chat in `profile` whose first user message is your `message`. Returns `{status: "started", ...}` or `{status: "rejected", reason}`.
- `apply_layout`, `gui_tour`, and `manage_connections` (connect shows the sign-in card).
There are no terminal, file, web, browser, memory, delegation, code execution or `clarify` tools here, and nothing installs in this chat; the task chat has all of that.
No terminal, file, web, browser, memory or `clarify` tools here.
## How to Run
The host facts line below is replaced by the JSON of `scripts/host_facts.py`, and the session facts follow the skill. Never run the script or ask for a fact the blocks carry.
Session facts: `surface` (`desktop`, `tui`, `cli` or a messaging platform; only `desktop` has cards and `start_chat`), `tools_present` (run a beat only when its tool is listed), `primary_profile` (the `profile` of `start_chat`), `guest_free_tier` (true on the no-account free tier), `setup_completed_at` (when setup handed off a task, else null).
Session facts follow the skill: `surface` (only `desktop` has cards and `start_chat`), `tools_present`, `primary_profile` (the `profile` of `start_chat`), `guest_free_tier`, `setup_completed_at`.
Host facts:
!`${HERMES_PYTHON} scripts/host_facts.py`
- `machine.*`: hardware and OS. Background only; never recite it unasked.
- `account.suggested_name` (the OS full name or null, never a login handle), `account.locale`, `account.locale_is_english`, `account.home_age_days`.
- `signals.machine_kind` (`Mac`, `PC`, `Spark` or `computer`: say it where the flow says "this computer"), `signals.machine_state` (`fresh`, `settling`, `established`, `unknown`), `looks_new`, `is_spark`, `machine_setup_leads` (the fork variant), `description` (one line for the machine-setup handoff).
- `plugin_tasks`: `[{id, label, plugins}]`, first tasks that bring their own plugins.
- `fork`: the fork card's `question`, and the rows the card shows by itself; beat 6 branches on their ids.
- `scan`, interpreted in code, or `{source: "unavailable"}`. Its field names say what they hold. Take care with `history_before_this_install` (a new machine is not a new user); `user_level`, `beginner_framing`, `runs_agents` (see Shape it to them); `apps_installed_no_use_seen` (weaker than `apps_used`); `crash_30d` (the only machine-health fact: mention it only when above 0, and only in the machine-setup handoff); `unknown` (not measured, never none or zero); `not_visible_at_tier` (hidden on purpose, never guess it).
- `machine.*`: background only; never recite it.
- `account.suggested_name` (or null), `account.locale_is_english`.
- `signals.machine_kind` (`Mac`, `PC`, `Spark`, `computer`: say it where the flow says "this computer"), `signals.looks_new`, `signals.is_spark`, `signals.machine_setup_leads`.
- `scan`: interpreted in code, or `{source: "unavailable"}`. `unknown` means not measured; never guess `not_visible_at_tier`.
When the host facts line still shows a command, host facts are unknown: suggest no name, and use "Know what you'd like it to make?" for the fork.
When the host facts line still shows a command, facts are unknown: suggest no name. The app fills the apps, plugins, tour, fork and machine_use rows itself. Host facts describe the backend machine; when the user disagrees, believe the user.
The apps and plugins cards start with the apps the scan saw in use, and Blender when it is here, already picked. Say nothing about that unless asked.
Work one beat at a time. First turn: when the history holds the name and accent answers, start at beat 1; otherwise run the Opening. Read nothing further yet.
Host facts describe the backend machine; when they and the user disagree, believe the user.
Work one beat at a time. When the history holds the name and accent answers, start at beat 1; otherwise run the Opening.
## Quick Reference
| # | Beat | Tool call |
|---|---|---|
| - | Welcome, name, accent | played by the app; see Opening |
| 1 | Apps they use | card `apps`; `manage_connections` only on request |
| 2 | Plugins for this computer | card `plugins` (records only) |
| - | Welcome, name, accent | played by the app |
| 1 | Apps they use | card `apps` |
| 2 | Plugins | card `plugins` (records only) |
| 3 | Layout | card `layout` |
| 4 | Tour offer | card `tour`; one `gui_tour` |
| 5 | The fork | card `fork` |
| 6 | Narrow to one task | card `machine_use`, or cards `project` and `tasks` |
| 7 | Handoff | `start_chat`, exactly once |
| 6 | Narrow to one task | card `machine_use` only for `machine` |
| 7 | Handoff | `start_chat`, once |
| 8 | After the handoff | one line, then stop |
The `setup_choose` arguments for each card. Copy the line whole. Fill only the `<slots>`; translate only `question` and the labels you write.
Copy each line whole; fill only the `<slots>`:
```
name {"kind":"question","question":"What should I call you?","options":[{"id":"suggested","label":"<account.suggested_name>"}],"multi_select":false}
@@ -85,147 +76,83 @@ layout {"kind":"layout","question":"Which layout?","options":[],"multi_select"
tour {"kind":"tour","question":"Want a look around first?","options":[],"multi_select":false}
fork {"kind":"fork","question":"<fork.question>","options":[],"multi_select":false}
machine_use {"kind":"machine_use","question":"What's this <signals.machine_kind> mainly for?","options":[],"multi_select":false}
project {"kind":"question","question":"<your question>","options":[],"multi_select":false}
tasks {"kind":"question","question":"<your question>","options":[<3 or 4 first tasks>],"multi_select":false}
```
`name0` is for a null `suggested_name`.
The other calls:
```
gui_tour {"action":"start","preset":"quick"} or {"action":"start","preset":"full"}
manage_connections {"action":"connect","connectors":["<id>", ...]}
start_chat {"profile":"<primary_profile>","title":"<task name, at most 40 characters>","message":"<the handoff message>"}
```
Tool rules, always:
- One `setup_choose` at a time.
- Before a card: the acknowledgment of the last answer, then the beat's own sentence or one light opinion, and nothing else. Every sentence is a statement ending in a full stop. The card shows its question under your text, so ask nothing, never name the next card's topic, start with no lead-in word (Now, Next, Right, Let's, One more thing), never write "next", and never list or describe the options. Bad: "Blender, noted. The layout next; you can change it any time." Good: "Blender, noted." then the card.
- Acknowledge a result with the pick's name (its `label`) and at most three plain words, never the same phrase twice: "Violet, done.", "Gmail, noted." No adjective or opinion about the pick and no word about the machine; opinions go before a pick, never after it. Bad: "That violet works.", "NVIDIA green, nice fit for that rig.", "Noted."
- Options you write yourself suit pills: at most six, labels of a few words, a `detail` only when the label cannot carry the point.
- Text typed instead of using the card is the answer. Never repeat a tool call that succeeded, except to re-send the pending card after an off-flow answer.
## Procedure
### Ground rules
### Voice
1. Never think out loud: every visible word is spoken to them. Never write "let me check", recap a step, or mention beats, cards, tools, facts or this skill. Never speak of them in the third person: "She wants to make something in Blender." is your reasoning leaking; say "Blender, then."
2. Never end a turn on a promise. A turn that says you will do something holds the call, then one line about the result.
3. When `account.locale_is_english` is false, write every visible word in that language from the first word, card labels included. If they write in another language, follow them.
4. Reusable text you draft for them goes in a fenced code block.
### Shape it to them
The cards and their lists are fixed; what you say around them and the first tasks you offer come from the facts and their words.
- Before a card you may offer one light opinion from the facts or their words ("Elite suits a day in the terminal."). Unasked, say what the scan saw only once: one "I noticed" line right before card `fork` (Beat 5) that shapes the first tasks, such as "You run agents and live in Slack, so I lean toward work you can hand off." Name at most two things, never a private detail (no counts, hosts, repos, file or browsing specifics). Outside that line, never tell them what the scan saw, and never state what kind of person they are: an installed app is a hunch that shapes what you offer; ask when it matters.
- Honesty: when they ask what you know about them or this computer, answer truthfully from the facts in a few plain lines: the machine basics, the apps this computer showed in use, and that Hermes scanned this computer when setup began and a short summary is part of this chat. Never deny the scan or claim to know less than the facts show; never guess `unknown` or `not_visible_at_tier`. Then re-send the pending card.
- When `scan.beginner_framing` is false, explain no basics; when `scan.runs_agents` is true, talk to them as someone who runs agents. Otherwise assume this is their first AI agent app: explain a feature in a plain sentence when their task needs it, with no glossary and no jargon such as harness or MCP.
- Only `signals.machine_state` `fresh` is a new machine: age is a setup heuristic, and Spark hardware alone never means a new device or OS install. Accept a correction and drop the new-machine framing.
- Models, when asked: the model picker chooses what answers them. For a local model, explain the download and hardware fit first (web search and apps keep their own services), then point to Settings, Providers, Local Models. Name no web search provider.
- Every visible word is spoken to them. Never think out loud, recap a step, or mention beats, cards, tools, facts or this skill.
- Before a card: the acknowledgment of the last answer, then at most one sentence of your own, all statements. The card shows its question, so never ask it, name the next topic, or list the options. No lead-in words (Now, Next, Let's).
- Acknowledge a pick by its label and at most three plain words, never the same twice: "Violet, done.", "Gmail, noted." No opinion after a pick.
- Short plain sentences, no em dashes, no exclamation marks. When `account.locale_is_english` is false, write in that language, labels included.
- Text typed instead of using the card is the answer. Never repeat a tool call that succeeded.
- Asked what you know about them: answer truthfully in a few plain lines (machine basics, apps seen in use, that Hermes scanned this computer when setup began), then re-send the pending card.
### Opening
When the name and accent results are in the history (the app played them), start at beat 1: the name is that `picked` (the suggested name when it is `suggested`).
Otherwise open it yourself: a short welcome in your own words, no tip or command ("Hey, come on in. I'm Hermes. Give me two minutes to set the place up around you, then we'll put me to work on something you actually want done."), then card `name` or `name0`. A "sure" or "yes" means the suggested name, exactly. Then one warm sentence about their name (not praise, no word about colour) and card `accent`. For a colour asked by name or hex, send card `accent` with one option `{"id":"#rrggbb","label":"<the colour's name>"}`.
If they give no name, never invent one, and leave "Call me" out of the handoff.
Without the app's name and accent answers in the history: a short welcome, card `name` (or `name0` when there is no suggested name), one warm sentence with their name, then card `accent`. "Sure" means the suggested name.
### Beat 1: apps they use
Acknowledge the colour by its name, then say in the same message that you would read and act inside these apps for them, not message them there, and set them up when you start on something: "Violet, done. I'd read and act inside these apps for you, your inbox, your calendar, your repos, not message you there, and I set them up when we start on something." When `guest_free_tier` is true, the next sentence is this one, once, word for word: "Wiring these up later wants a model provider: a free Nous account works, free tier, no card, or you can bring your own." Then card `apps`.
"<Colour>, done. I'd read and act inside these apps for you, not message you there, and I set them up when we start on something." When `guest_free_tier` is true, add once: "Wiring these up later wants a model provider: a free Nous account works, free tier, no card, or you can bring your own." Then card `apps`. With `no_answer` and a `notice`: one line that connecting apps needs a free Nous account and can wait, then follow `next`.
When card `apps` returns `no_answer` with a `notice`, no card was shown: connecting apps needs a Nous account and there is none here. Say one line in your own words that connecting apps needs a Nous account, which is free, and that it can be set up later, then follow `next`.
Connect an app here only when they ask: one `manage_connections` connect call with those apps. Chat apps like Discord or Telegram live in Messaging in the app's settings.
The picks go into the handoff; the task chat connects the ones the task needs. If they ask to connect an app right now, connect it here: one `manage_connections` connect call with every app they asked for, then one line on what came back. Never connect an app they did not ask for, never paste links, never describe a settings page (there is no Connectors page).
### Beat 2: plugins
Chat apps like Discord or Telegram are how people reach Hermes: say they live in Messaging in the app's settings.
### Beat 2: plugins for this computer
The acknowledgment, then one sentence: "Plugins are tools I install and run on this <signals.machine_kind>; picking one only records it." Then card `plugins`. When it returns `no_answer` with a `notice`, no card was shown because no plugin fits this computer: say nothing about plugins and follow `next`.
"Plugins are tools I install and run on this <machine_kind>; picking one only records it." Then card `plugins`. With `no_answer`, say nothing and follow `next`.
### Beat 3: layout
The acknowledgment, plus at most one light opinion from the facts, then card `layout`. The card applies the pick live; say nothing about the change.
Call `apply_layout` only for a layout asked for in words: `sidebar-left` (Basic, for talking to Hermes) or `terminal-deck` (Elite, for developers: terminal, files, diffs), or an id from its result.
The acknowledgment, at most one light opinion, then card `layout`. Call `apply_layout` only for a layout asked for in words: `sidebar-left` (Basic) or `terminal-deck` (Elite).
### Beat 4: the tour offer
The acknowledgment, then card `tour`. Then:
- `basics`: `gui_tour` with `preset:"quick"`; `tour`: `preset:"full"`.
- Both: ONE call, no `targets` and no `steps`, after one short line: "Here's where things live."
- `none`: no tour line.
Then go straight to beat 5 in the same turn, so the fork waits under the tour. A cancelled tour card means `none`. Never bring the tour up again. To show one part of the app later, call `targets`, then `start` with `steps` built only from what it reports (stable targets first); never invent a selector.
Card `tour`. `basics`: `gui_tour` preset `quick`; `tour`: preset `full`; one call after "Here's where things live." `none` or cancelled: no tour. Then beat 5 in the same turn. Never bring the tour up again.
### Beat 5: the fork
Before card `fork`, in your own words: "Ask me to show you any part of the app whenever you like. I'd rather build you something real than talk about it." Add the one "I noticed" line (Shape it to them) when the scan has something worth naming. Then card `fork`; the app fills its rows.
When `signals.machine_setup_leads` is true, the fork shows machine setup first and the rest behind "Something else". Add one sentence:
- `signals.looks_new`: "This <machine_kind> looks newly set up, and I can handle its updates, drivers and everyday tools." Never its age in days.
- Otherwise (a Spark of unknown or older age): name the hardware and offer to check its GPU, drivers and local AI tools. Never call it a new OS install.
- Never list installs before the machine audit.
"Ask me to show you any part of the app whenever you like. I'd rather build you something real than talk about it." When the scan has something worth naming, one "I noticed" line that names at most two things, never a private detail. When `signals.machine_setup_leads` is true, add one sentence: a newly set up machine ("I can handle its apps and everyday tools") or the Spark's hardware. Never its age in days. Then card `fork`: the app puts two first tasks built from their picks and the scan in front of "I have something in mind", "Help me set up this <machine_kind>" and "Let's figure it out together".
### Beat 6: narrow to one task
- `mind`, or a task typed in: decided. Go to beat 7.
- `machine`: the machine is the job. One line that frames it and asks nothing ("The Spark itself, then."), card `machine_use`, then hand off; the fork result's `handoff.plan` is the machine-setup plan. Never plan or list installs; the task chat audits first.
- A `plugin_tasks` id: decided. The ask is its label in the first person ("Help me make something in Blender."), plus what they want to make only if they said it. Its `plugins` join the install list.
- `figure`, `skip`, a cancelled card, or a skipped card: follow `next`. A card cancelled by a message they typed is not a skip.
- `automate`, or a general idea: card `project`, one short question about their real project or what they wish took less time, unless they already said. Then card `tasks`.
First-task options: short actions phrased as the outcome, at most one per app, so an installed app such as Blender earns ONE option. Prefer apps in `scan.apps_used`, one in `apps_installed_no_use_seen` only when their goals point to it, and name only apps they picked in beat 1. Fill the rest from their goals, one of them connection-free. Patterns, never a claim that an app is available: "Find emails that need a reply", "Find focus time around my meetings", "Catch me up on my project channel", "Turn my notes into next steps".
A pick or typed task is the decision, not a request for another menu; if "email" could mean several accounts, ask which once. Connector tasks use real data after permission, never a mock inbox; say in one clause that the task chat connects the app first. If they decline or the app is unavailable, keep the task honest about being blocked and let them pick another or supply the data. Never invent personal data or silently change the goal.
- A first-task row, `mind`, or a task typed in: decided, go to beat 7.
- `figure`: follow `next` and hand off with the ask "Let's figure out a first task together." The task chat offers the options.
- `machine`: one line that frames it ("The Spark itself, then."), card `machine_use`, then hand off with the machine plan. Never plan or list installs.
- A cancelled card: follow `next`.
### Beat 7: the handoff
One short sentence: the work gets its own chat so it has room, and this one stays open. Then `start_chat` once, with all three keys:
- `profile`: `primary_profile`. Without it the task opens here, with no terminal or file tools.
- `title`: the task's name.
- `message`: the handoff message, written exactly as the fork result's `handoff.message` says, with `handoff.plan` as its plan. The app appends a "What setup learned" block (their picks and the scan) under it; do not write that part. The task chat talks first: it offers a few directions in plain text and builds once they pick.
One short sentence: the work gets its own chat, and this one stays open. Then `start_chat` once with `profile` = `primary_profile`, `title` = the task's name, and `message` written as the fork result's `handoff.message` says, with `handoff.plan` as its plan. The app adds their picks, the scan and the first-task rules under it; write none of that.
### Beat 8: after the handoff
- `started`: one sentence of at most 15 words ("It's in its own chat now; I'm here under Welcome to Hermes if you need me."). No question, list, tip or sign-off. Then stop.
- `rejected`: say in plain words from the `reason` that it did not start, and retry once, with the same `profile`, only when they say yes. Never drop `profile`: the task would open here. Never claim a task is running without `started`.
- `started`: one sentence of at most 15 words ("It's in its own chat now; I'm here under Welcome to Hermes if you need me."). Then stop.
- `rejected`: say from the `reason` that it did not start; retry once, same `profile`, only when they say yes.
### Failure handling
- A card returns no pick or fails: take the default and move on (card `apps` with a `notice` gets its one line, see beat 1). Accent and layout keep what the app shows; apps and plugins record nothing; the tour is `none`; from the fork on, follow `next`. Say nothing about the empty answer. Never re-ask a skipped card in the same form.
- A beat skipped in words: move on without comment. When they stop setup, say only "It's all yours, and this chat stays here if you want a hand."
- Off the flow, even when their message cancelled a card: answer in a sentence or two, then re-send the card they left, once. For light or dark, send `{"kind":"theme","question":"Light or dark?","options":[],"multi_select":false}`.
- Every path ends in `start_chat` or that sentence; never end a turn on an open question with no card.
- Something only the task chat can do (a command, a file, a plugin install): say it happens there, fold it into the handoff message, and carry on. A plugin asked for after beat 2 counts as picked.
- After a relaunch mid-setup: continue from the first beat with no answer in the history.
- A card with no pick: take the default and move on silently; from the fork on, follow `next`. Never re-ask a skipped card in the same form.
- They stop setup: say only "It's all yours, and this chat stays here if you want a hand."
- Off the flow: answer in a sentence or two, then re-send the pending card once. Light or dark: `{"kind":"theme","question":"Light or dark?","options":[],"multi_select":false}`.
- Something only the task chat can do (a command, a file, an install): say it happens there and put it in the ask.
- After a relaunch: continue from the first beat with no answer in the history.
### Surface fallback
When `surface` is not `desktop` or a tool is not in `tools_present`:
- No `setup_choose`: ask in plain text, one question per message, options in one short sentence. Ask the name yourself; skip accent and layout. On messaging or a remote server, ask about the apps they use first and in more depth.
- No `gui_tour`: skip the tour offer; once, say they can ask for a tour in any chat.
- No `start_chat`: start the task here, with what they said as your brief. No `manage_catalog`: give `hermes plugins install <id>` for each plugin the task needs.
- You are still Hermes, never "Setup": short plain sentences, no em dashes, no exclamation marks, no stock lines. Never call an absent tool or mention that one is missing.
Without `setup_choose`: ask in plain text, one question per message; skip accent and layout. Without `gui_tour`: skip the tour. Without `start_chat`: start the task here. Never mention a missing tool.
## Pitfalls
- Repeating the card's question in text.
- A handoff message missing a part: the task chat sees only it and the app's "What setup learned" block.
- Planning or starting the task here instead of handing it off.
## Verification
- With the app's opening in the history, your first message acknowledges the accent and ends in card `apps`; without it, it ends in card `name` or `name0`.
- Beats follow the Quick Reference, one card at a time; every `setup_choose` call carries `options`.
- Any `manage_connections` call carries only apps the user asked to connect now.
- Exactly one `start_chat` returned `started`, with `profile` = `primary_profile` and a `message` holding every non-empty part.
- Exactly one `start_chat` returned `started`, with `profile` = `primary_profile`.
- After `started` there is one short line and no question.

View File

@@ -50,7 +50,6 @@ _HANDLE_CHARS = re.compile(r"[\d_@/\\]")
_SPARK_MODEL = re.compile(r"\b(dgx|spark|gb10)\b", re.I)
_FORK_QUESTION = "Know what you'd like it to make?"
_FALLBACK_QUESTION = "What sounds better?"
SCAN_DIR = Path(__file__).resolve().parent / "userscan"
# The scanner package's import name. It is private so that loading it never adds SCAN_DIR (and its
@@ -283,27 +282,53 @@ def _description(*, looks_new: bool, age: int | None, spark: bool, gpu: str, cpu
return ", ".join(part for part in parts if part)
def _fork(kind: str, leads: bool, plugin_tasks: list[dict]) -> dict:
"""Fork options in the order the flow pins. Ids are stable; labels may be translated."""
mind = {"id": "mind", "label": "I have something in mind"}
automate = {"id": "automate", "label": "Automate something I already do"}
machine = {"id": "machine", "label": f"Help me set up this {kind}"}
figure = {"id": "figure", "label": "Let's figure it out together"}
skip = {"id": "skip", "label": "Skip this for now"}
tasks = [{"id": task["id"], "label": task["label"]} for task in plugin_tasks]
if leads:
return {
"question": _FORK_QUESTION,
"options": [machine, {"id": "something_else", "label": "Something else"}],
"fallback_question": _FALLBACK_QUESTION,
"fallback_options": [mind, automate, *tasks, figure, skip],
}
return {
"question": _FORK_QUESTION,
"options": [mind, automate, machine, *tasks, figure, skip],
"fallback_question": None,
"fallback_options": [],
}
def _fork(kind: str) -> dict:
"""The fork card's fixed rows; ``fork_card`` puts first tasks from the setup picks in front. Ids are stable;
labels may be translated."""
return {"question": _FORK_QUESTION, "options": [
{"id": "mind", "label": "I have something in mind"},
{"id": "machine", "label": f"Help me set up this {kind}"},
{"id": "figure", "label": "Let's figure it out together"},
]}
# Connector picks a daily brief has nothing to read from.
_NOT_BRIEF = frozenset({"discord", "spotify", "steam", "telegram", "whatsapp", "youtube", "twitch", "reddit"})
def _from_picks(cards: dict) -> list[dict]:
picks, ids = cards.get("picks") or {}, cards.get("pick_ids") or {}
apps = [str(label) for label in picks.get("connectors") or () if str(label).lower() not in _NOT_BRIEF]
plugins = set(ids.get("plugins") or ())
rows = [{"id": "brief", "label": f"A daily brief from {' and '.join(apps[:2])}"}] if apps else []
rows += [{"id": task_id, "label": task["label"]} for task_id, task in (cards.get("plugin_tasks") or {}).items()
if plugins & set(task["plugins"])]
return rows + ([{"id": "week", "label": "A summary of my week"}] if apps else [])
def _from_machine(suggest: dict) -> list[dict]:
kind = suggest.get("kind") or "computer"
if suggest.get("spark"):
return [{"id": "apps", "label": f"Install a few apps for this {kind}"},
{"id": "local_model", "label": "Set up a local model"}]
return [{"id": "apps", "label": f"Install a few apps for this {kind}"}] if suggest.get("fresh") else []
def _from_nothing(suggest: dict) -> list[dict]:
app = next(iter(suggest.get("apps_used") or ()), None)
page = f"A cheat sheet page for {app}" if app else f"A one-page guide to this {suggest.get('kind') or 'computer'}"
return [{"id": "page", "label": page}, {"id": "script", "label": "A quick script that tidies my Downloads"}]
def fork_card(cards: dict) -> dict:
"""The fork card when it is shown: two first tasks, each finishable in minutes, from the apps and plugins
picked earlier in this setup and the scan, in front of the fixed rows. One from the picks and one from the
machine when both have one."""
suggest = cards.get("suggest") or {}
picks, machine = _from_picks(cards), _from_machine(suggest)
ordered = picks[:1] + machine[:1] + picks[1:] + machine[1:] + _from_nothing(suggest)
fork = cards.get("fork") or _fork(suggest.get("kind") or "computer")
return {**fork, "options": ordered[:2] + fork["options"]}
def _hermes_home() -> Path | None:
@@ -771,31 +796,20 @@ def collect(scanned: Callable[[float], tuple[dict | None, str]] | None = None) -
),
},
"plugin_tasks": plugin_tasks,
"fork": _fork(kind, leads, plugin_tasks),
"fork": _fork(kind),
"scan": scan,
}
def _handoff(facts: dict) -> dict:
"""The handoff message's parts and its two plans from ``templates/handoff.md``, the machine plan cut to
this computer, so the model reads them only when it reaches the handoff."""
"""The handoff message's parts and its two plans from ``templates/handoff.md``, the machine plan naming this
computer, so the model reads them only when it reaches the handoff."""
text = (Path(__file__).resolve().parent.parent / "templates" / "handoff.md").read_text(encoding="utf-8")
sections = dict(re.findall(r"^## (\S+)\n\n(.*?)\n*(?=^## |\Z)", text, re.M | re.S))
machine, scan = facts["machine"], facts.get("scan") or {}
arm = str(machine["native_arch"]).lower() in ("arm64", "aarch64")
crashes = scan.get("crash_30d")
parts = [
sections["machine"].replace("<description>", facts["signals"]["description"]),
sections["machine-crashes"].replace("<crash_30d>", str(crashes)) if isinstance(crashes, int) and crashes else "",
sections["machine-nvidia"] if machine["gpu_class"] == "nvidia" else "",
sections["machine-plan"],
sections.get(f"machine-drivers-{machine['os_family']}", ""),
sections["machine-arm"] if arm else "",
sections.get(f"machine-arm-nvidia-{machine['os_family']}", "") if arm and machine["gpu_class"] == "nvidia" else "",
sections["machine-end"],
]
signals = facts["signals"]
machine = sections["machine"].replace("<machine_kind>", signals["machine_kind"])
return {"message": sections["message"], "build": sections["build"],
"machine": '"' + " ".join(part for part in parts if part) + '"'}
"machine": machine.replace("<description>", signals["description"])}
_LEVELS = {"beginner": "new to AI agent apps", "power-user": "a power user", "expert": "an expert"}
@@ -816,17 +830,26 @@ def _learned(facts: dict) -> list[str]:
level = f"{level}, a developer" if level else "a developer"
if level:
lines.append(f"I am {level}.")
crashes = scan.get("crash_30d")
if isinstance(crashes, int) and crashes:
lines.append(f"It shut down unexpectedly {crashes} time{'' if crashes == 1 else 's'} in the last 30 days.")
return lines
def setup_cards(facts: dict) -> dict:
"""What the setup cards take from these facts: the fork rows, the rows the apps and plugins cards start
with picked (apps seen in use; Blender when it is here), the handoff text the fork result carries, and
the scan lines ``start_chat`` appends to the task chat's first message."""
"""What the setup cards take from these facts: the fork rows and what its first tasks are built from, the
rows the apps and plugins cards start with picked (apps seen in use; Blender when it is here), the handoff
text the fork result carries, and the scan lines ``start_chat`` appends to the task chat's first message."""
used = (facts.get("scan") or {}).get("apps_used") or []
signals = facts["signals"]
return {
"learned": _learned(facts),
"fork": facts["fork"],
# What ``fork_card`` builds first tasks from, beside the picks the cards record.
"suggest": {"kind": signals["machine_kind"], "spark": signals["is_spark"], "fresh": signals["looks_new"],
"apps_used": used[:3]},
"plugin_tasks": {task["id"]: {"label": task["label"], "plugins": task["plugins"]}
for task in facts["plugin_tasks"]},
"preselected": {
"connectors": [_SCAN_CONNECTORS[app] for app in used if app in _SCAN_CONNECTORS],
"plugins": ["blender"] if facts["signals"].get("has_blender") else [],

View File

@@ -1,62 +1,21 @@
# Handoff message
`scripts/host_facts.py` sends these sections with the fork card's result, the machine plan cut to this computer. Each `## ` heading names one section.
`scripts/host_facts.py` sends these sections with the fork card's result. Each `## ` heading names one section.
## message
The handoff message is the new chat's first user message. It is visible, so write it as their own ask, in their language, in the first person. The app adds one block under it, "What setup learned about me": their name, app, plugin and layout picks, and what the scan saw. Do not list those picks or scan facts again. Nothing else reaches the task chat: no memory, no hidden note.
The handoff message is the new chat's first user message. It is visible, so write it as their own ask, in their language, in the first person. The app adds the rest under it: a folded "What setup learned about me" block (their name, app, plugin and layout picks, and what the scan saw) and the first-task rules, which connect the apps and install the plugins they picked before the task starts. Do not repeat any of that, and add nothing they did not say.
The install list: the plugins they picked that the task needs (all of them for machine setup or a task naming the app), plus a picked first task's own `plugins`. The connect list: apps the task needs that setup did not connect; none for machine setup.
Write these parts in order, one short paragraph each (part names are not text). Drop a part whose slot is empty.
Write these parts in order, one paragraph each (part names are not text). Copy the quoted wording, fill only the `<slots>`, and drop a sentence, clause or part whose slot is empty or whose condition does not hold. Add nothing they did not say. Check every non-empty part is there.
1. The ask, in one or two sentences, in their words where they gave them, keeping the named app and the outcome. No machine specs.
2. "Call me <name>." Then what they are working on, if they said it.
3. "Connected during setup: <apps setup connected>."
4. Only with an install or connect list: "Once I pick a direction or say go, before building: install <install list> with one `manage_catalog` install call carrying all of them, then connect <connect list> with one `manage_connections` connect call carrying all of them. The ids are exact; skip search and status checks, and start the work when they return. If I skip some, go on without them and tell me in one line what each would have added; do not offer them again." Drop the install or the connect clause when its list is empty. With an install list, add: "Find plugin tools with `tool_search` and read a plugin's skill with `skill_view` by its exact name; if its app is not running, tell me plainly." With other plugin picks, add: "Picked during setup, not needed yet: <other plugin picks>."
5. The plan below, word for word.
6. Last (drop the first sentence when `scan.beginner_framing` is false): "I'm new to AI agent apps: when a feature first matters, explain it in a sentence or two, no jargon. When you start building, tell me in one short sentence that you'll ask for permissions as you go and I can say no or redirect you. When the first pass is done, ask me whether it matches what I wanted, with Looks right, Change something, and Take it further, and act on my pick."
1. The ask, in one or two sentences, in their words where they gave them, keeping the named app and the outcome. A fork row they picked is the ask as its label says it ("A daily brief from Linear and Slack."). "I have something in mind" with nothing typed stays as it is. No machine specs.
2. What they are working on, if they said it, and any plugin they asked for after the plugins card.
3. The plan below, word for word.
## build
"Talk with me before you build. In your first reply, say in two or three plain lines what you take from what setup learned, then ask me in plain text, not a card, one short question with two to four concrete directions drawn from my apps and this computer, like: Should I start with A, B or C? Or tell me more about what you want. Name the apps each direction needs and offer to connect them. Build only after I pick one or say go; then plan briefly and build. Use real data only from connected apps and find their tools with `tool_search`; tools already signed in on this computer, like a logged-in gh, are fair to use, and say so in one line. Never route around a connector: no IMAP client, app password or other way into the same account; if I decline one, that app is out of this build. Ask before sending, deleting or scheduling anything, and set up no recurring job unless I asked. Never use mock or sample data." With a connect list, add: "Include at least one real reading or action through a connected app." Without one, add: "Make this first version finishable with no account I have not connected: web research with the browser visible to me, scripts, a small app, a file-based tracker, a generated page. If the idea needs an account, build the no-account core first and offer the connection next."
Keep the first pass small: I would like to see something done in a few minutes.
## machine
Get this computer ready to use, end to end, with the terminal. It needs no account; never send me to a sign-in for it. Setup signals, not proof of the machine's age: <description>. I mainly use it for <their answer>. Look first: OS and version, architecture, pending updates, free disk, the package manager, and which everyday tools are installed (browser, editor, git, python, node, docker, the apps I named).
## machine-nvidia
Also check the GPU and driver (nvidia-smi), a container runtime and the CUDA toolchain.
## machine-plan
Tell me what you found in a few plain lines. Match the plan to my use: email, calendars, documents and meetings need no developer stack. Recommend WSL or CUDA only as a verified need of my use, benefit first; never WSL on Linux or macOS, never a CUDA reinstall just because this is a Spark. Propose a short numbered plan, most useful first: updates, a package manager if missing, my everyday tools, sane defaults, then anything exotic. Ask before running it, with Go ahead, Change the list, and Just the essentials. Then one step at a time, one line per step on what it is for. Prefer native, already-working tools and the official package manager over downloaded installers. Never install what I did not agree to, overwrite config without asking, or disable security settings; stop and ask when anything looks destructive or wants a password I did not give.
## machine-drivers-win32
Drivers: check for missing or unknown devices and vendor GPU drivers, and say so when Windows already handles it.
## machine-drivers-darwin
Drivers: system updates and the App Store cover drivers on macOS, so say that.
## machine-drivers-linux
Drivers: check the kernel and driver pairing before touching the GPU.
## machine-arm
On Arm, check the architecture for every install, prefer native arm64 builds, say when only an emulated x64 one exists, and never assume a tool has an Arm release.
## machine-arm-nvidia-win32
On an Arm Windows PC with NVIDIA silicon, treat CUDA and anything GPU-related as arm64-specific and verify the build first.
## machine-end
Anything needing my sign-in, a licence key or a payment goes on a list for me. Finish with what changed, what you skipped and why, what is left for me, and whether a reboot is needed.
## machine-crashes
It shut down unexpectedly <crash_30d> times in the last 30 days; find out why as part of the look.
Help me set up this <machine_kind> (<description>). I mainly use it for <their answer>. Start with a few apps I'll want.

View File

@@ -21,14 +21,8 @@ _MACHINE_USE_ROWS = [
{"id": "creative", "label": "Creative"},
{"id": "mix", "label": "A bit of everything"},
]
# The fork when the /initiate-setup turn recorded none (host facts unknown).
_FORK = {"question": "Know what you'd like it to make?", "options": [
{"id": "mind", "label": "I have something in mind"},
{"id": "automate", "label": "Automate something I already do"},
{"id": "machine", "label": "Help me set up this computer"},
{"id": "figure", "label": "Let's figure it out together"},
{"id": "skip", "label": "Skip this for now"},
]}
# The fork question when the /initiate-setup turn recorded none (host facts unknown).
_FORK_QUESTION = "Know what you'd like it to make?"
_NO_ANSWER = ("The card got no answer: it timed out, the turn was interrupted, or no Hermes desktop "
"window answered.")
# From the fork on, each skipped card steps down this ladder, so setup ends in a handoff or a stop.
@@ -43,6 +37,8 @@ _SKIP_LADDER = (
_NO_CONNECTORS = ("Connecting apps needs a Nous account (free) and none is signed in here, so no card was shown; "
"it can be set up later.")
_NO_PLUGINS = "No plugins on the list run on this computer, so no card was shown."
# The task chat's first-task skill offers the options for a vague ask, so setup hands off at once.
_FIGURE = "Hand off now with start_chat; the ask is \"Let's figure out a first task together.\""
_MACHINE_USE_SKIPPED = "Hand off with the machine-setup plan and leave their use out."
_RESEND = ("If this text does not answer the card, answer it in a sentence or two, then send this card again in "
"the same turn: {card}")
@@ -60,7 +56,7 @@ _THEN: dict[str, Callable[[dict, object], str]] = {
"layout": lambda cards, picked: "Send card tour: " + _card("tour", "Want a look around first?"),
"tour": lambda cards, picked: (
("After the gui_tour call, send" if picked in ("basics", "tour") else "No tour. Send")
+ " card fork in this same turn: " + _card("fork", (cards.get("fork") or _FORK)["question"])),
+ " card fork in this same turn: " + _card("fork", (cards.get("fork") or {}).get("question") or _FORK_QUESTION)),
}
@@ -138,8 +134,7 @@ def _follow_up(kind: str, card: str, result: dict, rows: Optional[list], cards:
state["step"] = min(max(step + 1, shape), len(_SKIP_LADDER) - 1)
extra["next"] = _SKIP_LADDER[state["step"]]
elif kind == "fork" and picked == "figure":
state["step"] = max(step, 0)
extra["next"] = _SKIP_LADDER[state["step"]]
extra["next"] = _FIGURE
elif outcome == "submitted" and isinstance(picked, str) and rows and picked not in {row["id"] for row in rows}:
extra["next"] = _RESEND.format(card=card)
elif kind in _THEN:
@@ -152,19 +147,36 @@ _REMEMBERED = frozenset({"connectors", "plugins", "layout"})
def _remember(kind: str, question: str, result: dict, state: dict) -> dict:
"""Also the connector and plugin ids (the task chat connects and installs them first) and the fork pick (a
plugin task's plugins join those installs)."""
from agent.initiate_setup_prompt import NAME_QUESTION
key = "name" if kind == "question" and question == NAME_QUESTION else kind if kind in _REMEMBERED else None
if key is None or result["outcome"] != "submitted":
if result["outcome"] != "submitted":
return state
return {**state, "picks": {**state.get("picks", {}), key: result.get("label") or result["picked"]}}
picked = result["picked"]
if kind == "fork":
return {**state, "fork_pick": picked}
key = "name" if kind == "question" and question == NAME_QUESTION else kind if kind in _REMEMBERED else None
if key is None:
return state
state = {**state, "picks": {**state.get("picks", {}), key: result.get("label") or picked}}
if kind in ("connectors", "plugins") and isinstance(picked, list):
state["pick_ids"] = {**state.get("pick_ids", {}), kind: picked}
return state
def _fork_rows(cards: dict) -> list:
# Built when the card is shown: the apps and plugins cards before it have been answered by then.
from agent.initiate_setup_prompt import fork_card
return fork_card(cards)["options"]
# App-owned parts of a card, filled here from the recorded facts so the model can neither drop nor edit them.
_APP_FILLED: dict[str, Callable[[dict], dict]] = {
"tour": lambda cards: {"options": _TOUR_ROWS, "multi_select": False},
"machine_use": lambda cards: {"options": _MACHINE_USE_ROWS, "multi_select": False},
"fork": lambda cards: {"options": (cards.get("fork") or _FORK)["options"], "multi_select": False},
"fork": lambda cards: {"options": _fork_rows(cards), "multi_select": False},
"connectors": lambda cards: {"preselected": (cards.get("preselected") or {}).get("connectors") or [],
"multi_select": True},
"plugins": lambda cards: {"preselected": (cards.get("preselected") or {}).get("plugins") or [],
@@ -229,11 +241,6 @@ def setup_choose_tool(kind: str = "", question: str = "", options=None, multi_se
"next": _THEN[kind](cards, None)}, ensure_ascii=False)
payload.update(_APP_FILLED[kind](cards) if kind in _APP_FILLED else {})
reply = callback(payload)
fork = cards.get("fork") if kind == "fork" else None
# "Something else" on a machine-first fork opens the rest of the fork in the same call.
if fork and fork.get("fallback_options") and (reply or {}).get("picked") == "something_else":
payload = {**payload, "question": fork["fallback_question"], "options": fork["fallback_options"]}
reply = callback(payload)
result = _result(reply, payload["options"])
extra, state = _follow_up(kind, card, result, payload["options"], cards)
state = _remember(kind, text, result, state)

View File

@@ -305,7 +305,9 @@ def _history_to_messages(history: list[dict], *, profile_home=None, image_urls:
if _is_display_hidden_marker(role, content_text):
continue
if role == "user":
content_text = _DISCORD_TRIGGERING_NOTE_RE.sub(r"\1", content_text)
# A setup handoff's first message carries the first-task skill after what the user sees.
from agent.first_task_prompt import visible_text
content_text = visible_text(_DISCORD_TRIGGERING_NOTE_RE.sub(r"\1", content_text))
if role == "assistant" and m.get("tool_calls"):
for tc in m["tool_calls"]:
fn, tc_id = tc.get("function", {}), tc.get("id", "")

View File

@@ -21,10 +21,16 @@ def _setup_learned(cards: dict) -> str:
lines = [f"- {label}: {', '.join(map(str, value)) if isinstance(value, list) else value}"
for key, label in _PICK_LINES if (value := picks.get(key))]
lines += [f"- {line}" for line in cards.get("learned") or ()]
if not lines:
return ""
return "\n".join(["What setup learned about me:", *lines,
"Start from this; look at my computer again only when the task needs more."])
return "\n".join(["What setup learned about me:", *lines]) if lines else ""
def _first_steps(cards: dict) -> tuple[list, list]:
"""The connector and plugin ids the task chat sets up before its first task: the apps and plugins picked in
setup, plus the plugins of a plugin task picked at the fork."""
ids = cards.get("pick_ids") or {}
task = (cards.get("plugin_tasks") or {}).get(cards.get("fork_pick") or "") or {}
install = [*(ids.get("plugins") or ()), *(task.get("plugins") or ())]
return list(ids.get("connectors") or ()), list(dict.fromkeys(install))
def start_chat(args: dict, caller_id: str | None = None) -> str:
@@ -54,9 +60,12 @@ def start_chat(args: dict, caller_id: str | None = None) -> str:
target_home = Path(home or server._hermes_home)
from_setup = (caller_home / SETUP_PROFILE_MARKER).is_file()
if from_setup:
from agent.first_task_prompt import first_task_tail
with server._session_profile_runtime_scope({"profile_home": str(caller_home)}, hydrate_secrets=False):
learned = _setup_learned(read_cards(caller.get("session_key") or ""))
message = f"{message}\n\n{learned}" if learned else message
cards = read_cards(caller.get("session_key") or "")
learned = _setup_learned(cards)
message = (f"{message}\n\n{learned}" if learned else message) + first_task_tail(*_first_steps(cards))
token = bind_transport(caller.get("transport"))
try:
with server._session_profile_runtime_scope({"profile_home": str(home) if home else None}):

View File

@@ -9,7 +9,7 @@ Guided onboarding suggests outcomes from the user's app preferences and observed
## What takes priority
The existing `machineSetupLeads()` decision still leads with machine setup for a newly set-up machine or a recognized RTX/DGX Spark, with other tasks behind **Something else**. The fresh-machine signal is the existing age heuristic, not proof of the OS installation date. A Spark with unknown age gets a hardware-specific callout, not a claim that its OS is new.
The fork card leads with two first tasks built from the setup picks and the scan; a recognized RTX/DGX Spark or a newly set-up machine gets "Install a few apps for this <machine>" among them, and "Help me set up this <machine>" is always a row. The fresh-machine signal is the existing age heuristic, not proof of the OS installation date. A Spark with unknown age gets a hardware-specific callout, not a claim that its OS is new.
Otherwise the guide favors relevant app-backed tasks, with a connection-free alternative. Detecting an app earns at most one option; the remaining choices come from the user's goals and other capabilities. Several ideas for the same app are appropriate only when explicitly requested. Choosing an inbox task means using real inbox data after permission, not building a mock inbox when permission is missing. A skipped or unavailable required connection leaves that task blocked; the user can choose a different task or supply data.

View File

@@ -215,6 +215,7 @@ hermes skills uninstall <skill-name>
|-------|-------------|
| [**canvas**](../user-guide/skills/optional/productivity/productivity-canvas.md) | Fetch Canvas LMS courses and assignments via API token. |
| [**decision-questionnaire**](../user-guide/skills/optional/productivity/productivity-decision-questionnaire.md) | Turn an unanswerable decision into a questionnaire doc. |
| [**first-task**](../user-guide/skills/optional/productivity/productivity-first-task.md) | Run the first task chat that setup hands off. |
| [**here-now**](../user-guide/skills/optional/productivity/productivity-here-now.md) | Publish sites to &#123;slug&#125;.here.now and store files in Drives. |
| [**initiate-setup**](../user-guide/skills/optional/productivity/productivity-initiate-setup.md) | Run the first-run setup chat in the Hermes desktop app. |
| [**live-dashboard**](../user-guide/skills/optional/productivity/productivity-live-dashboard.md) | Build self-updating dashboards from live sources. |

View File

@@ -194,7 +194,7 @@ Enabled by the desktop setup profile's own config (`platform_toolsets.cli`). Onl
| Tool | Description | Requires environment |
|------|-------------|----------------------|
| `setup_choose` | Only through the `setup` toolset, desktop sessions only; never deferred unless a `tools.tool_search.defer` list names it. Shows one card in the setup chat: `kind` is `question`, `accent`, `theme`, `layout`, `connectors`, `plugins`, `tour` or `fork`, and `question` is the card's heading. `options` is required: up to 12 `{id, label, detail?}` rows (unique ids) replace the app's own list, and `[]` shows the app's list for a picker or asks for free text for kind `question`. `tour` (Quick tour, Show me everything, Skip) and `fork` (the rows the `/initiate-setup` turn computed from host facts; "Something else" opens the rest in the same call) always show the backend's rows. The connectors and plugins cards start with the apps the setup scan saw in use, and Blender when it is present, already picked. A card with options still takes a typed answer. `multi_select` lets the user pick several rows. An accent, theme or layout pick applies in the app as soon as the user picks it. The result has `outcome` (`submitted`, `cancelled` or `no_answer` with a `notice`), `picked` (an option id or typed text, or a list of ids with `multi_select`), and `label` (the name the card showed for each pick, such as `Ultraviolet` for `#8a2be2`) when the pick is one of the card's rows. When the connectors list is unavailable (no Nous identity, after one guest sign-up attempt) or the plugins list is empty, the call returns `no_answer` with a `notice` at once and shows no card. It stores nothing. Anywhere else the call returns an error. | — |
| `setup_choose` | Only through the `setup` toolset, desktop sessions only; never deferred unless a `tools.tool_search.defer` list names it. Shows one card in the setup chat: `kind` is `question`, `accent`, `theme`, `layout`, `connectors`, `plugins`, `tour` or `fork`, and `question` is the card's heading. `options` is required: up to 12 `{id, label, detail?}` rows (unique ids) replace the app's own list, and `[]` shows the app's list for a picker or asks for free text for kind `question`. `tour` (Quick tour, Show me everything, Skip) and `fork` (two first tasks built, when the card shows, from the apps and plugins picked earlier and the setup scan, then "I have something in mind", "Help me set up this <machine>" and "Let's figure it out together") always show the backend's rows. The connectors and plugins cards start with the apps the setup scan saw in use, and Blender when it is present, already picked. A card with options still takes a typed answer. `multi_select` lets the user pick several rows. An accent, theme or layout pick applies in the app as soon as the user picks it. The result has `outcome` (`submitted`, `cancelled` or `no_answer` with a `notice`), `picked` (an option id or typed text, or a list of ids with `multi_select`), and `label` (the name the card showed for each pick, such as `Ultraviolet` for `#8a2be2`) when the pick is one of the card's rows. When the connectors list is unavailable (no Nous identity, after one guest sign-up attempt) or the plugins list is empty, the call returns `no_answer` with a `notice` at once and shows no card. It records the picks for the handoff: `start_chat` hands the picked apps and plugins to the task chat, which connects and installs them first. Anywhere else the call returns an error. | — |
## `start_chat` toolset

View File

@@ -0,0 +1,131 @@
---
title: "First Task — Run the first task chat that setup hands off"
sidebar_label: "First Task"
description: "Run the first task chat that setup hands off"
---
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
# First Task
Run the first task chat that setup hands off.
## Skill metadata
| | |
|---|---|
| Source | Optional — install with `hermes skills install official/productivity/first-task` |
| Path | `optional-skills/productivity/first-task` |
| Version | `0.1.0` |
| Author | Siddharth Balyan (alt-glitch) + Hermes Agent |
| License | MIT |
| Platforms | linux, macos, windows |
| Tags | `onboarding`, `first-run`, `desktop`, `handoff` |
| Related skills | [`initiate-setup`](../../optional/productivity/productivity-initiate-setup.md) |
## Reference: full SKILL.md
:::info
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
:::
# First Task Skill
Runs the first chat after setup, the one `start_chat` opened. The top of this message is the user's ask, then "What setup learned about me" (their picks and the scan), then these rules and a JSON block. The goal: they see work within one minute and hold a finished, useful result within five.
## When to Use
- This message carries the skill under a handoff from setup.
- Until the first result lands. After that it is a normal chat.
## Prerequisites
The task chat's own tools: `manage_connections`, `manage_catalog`, `clarify`, `terminal`, the file tools, the browser, `tool_search`, `skill_view`, and `desktop_preview` in the desktop app. Use only tools that are in your tool list.
## How to Run
The JSON block at the end holds `connect`, the connector ids of the apps they picked in setup, and `install`, the catalog plugin ids they picked or their first task brings. The ids are exact: skip search and status checks.
The "What setup learned" lines are your recon. Do not survey the machine first: no system scan, no update search, no inventory of installed tools. Look at one thing only when the next step needs it.
## Quick Reference
| Step | What | Call |
|---|---|---|
| 1 | One line on what you start with | none |
| 2 | Connect the picked apps | `manage_connections` connect, every id in `connect`, once |
| 3 | Install the picked plugins | `manage_catalog` install, every id in `install`, once |
| 4 | Specific ask: start it. Vague ask: three options | `clarify`, three choices |
| 5 | The first slice, finished in five minutes | the task's own tools |
| 6 | Show the result, offer the next step | `clarify`: Looks right, Change something, Take it further |
```
manage_connections {"action":"connect","connectors":["<id>", ...]}
manage_catalog {"action":"install","items":[{"kind":"plugin","id":"<id>"}, ...],"reason":"<one line>"}
clarify {"questions":[{"question":"<short question>","choices":["<option>","<option>","<option>"]}]}
```
## Procedure
### 1. Connect first
Your first reply is one short line on what you will start with, then steps 2 and 3 back to back, before any other work. Skip a step whose list is empty. A skipped or failed row never blocks: say in one line what it would have added, go on without it, and never offer it again in this chat.
### 2. Ask only when vague
- A specific ask (a named outcome such as "A daily brief from Linear and Slack", "Install a few apps for this Spark", a scene in Blender): start it now, with no confirming question.
- A vague ask ("I have something in mind", "Let's figure out a first task together", "help with my work"): one `clarify` card with three options, then start the pick. Text they type instead is the pick.
Options are outcomes of a few words, each finishable in five minutes, built from their picks and the scan. Name only apps they picked or the scan saw in use.
- Work apps picked (Linear, Slack, Gmail, Calendar): "A daily brief from Linear and Slack", "A summary of my week", "Learn how I work from my tools".
- An NVIDIA or Spark machine: "Install a few apps for this Spark", "Set up a local model".
- A plugin picked: one small thing in that app, such as "A simple scene in Blender".
- Nothing picked: "A small HTML page about &lt;something from the scan>", a quick useful script.
### 3. Time box
- Within one minute they see work: a file being written, a page opening, a first install.
- Within five minutes they hold a finished result: a page open in the preview, a brief in the chat, apps installed.
- A big ask gets cut to a first slice. Say so in one line ("Setting up the whole Spark is big; first I'll install a few apps you'll want."), finish that slice, then offer the next one.
- A step that runs long (a large download, an update search, a full build) is never the first slice: offer it as the next step.
- No written plan before work, except the machine interview below.
### 4. Machine setup is an interview
For "Help me set up this &lt;machine>" or "Install a few apps":
1. Two or three short `clarify` cards, one at a time, two to four choices each: "These apps?" (three to five everyday apps that fit their use and the apps they named), "Install the NVIDIA tools?" (NVIDIA machines only), "A local model?" (a Spark or a strong GPU only).
2. Then the smallest useful part: install the apps they chose with the official package manager, one line per install.
3. On Arm, check each install has a native arm64 build and say when only an x64 one exists.
4. Anything that needs a password, a licence or a payment goes on a short list for them. Never disable security settings or overwrite config without asking.
Finish with what changed and one offer for the next slice (drivers, a local model, developer tools).
### 5. Build rules
- Real data only: from connected apps (find their tools with `tool_search`) or tools already signed in on this computer, such as a logged-in `gh` (say so in one line). Never mock or sample data. Never route around a connector: no IMAP client, app password or scraping into the same account.
- Ask before sending, deleting or scheduling anything. Set up no recurring job unless they asked.
- A generated page is one self-contained HTML file, opened with `desktop_preview`.
- A plugin's tools: find them with `tool_search`, and read its skill with `skill_view` by its exact name. If its app is not running, say so plainly.
- When "What setup learned" says they are new to AI agent apps: explain a feature in one plain sentence when it first matters, with no jargon. The first time you act on the computer, say once that you ask for permission as you go and they can say no.
### 6. Close the first slice
One short line on what you made and where it is, then a `clarify` card with "Looks right", "Change something" and "Take it further". Act on the pick.
## Pitfalls
- A survey of the machine before any work. The facts block is the survey.
- A plan question when the ask was already specific.
- A 30-minute job as the first slice.
- Offering again an app they skipped.
- Reading the facts block back to them.
- Stiff words: write short, plain, warm sentences, with no filler, em dashes or exclamation marks.
## Verification
- The first reply holds one line of text, then the connect and install calls.
- Visible work starts within one minute and the first result lands within five.
- A vague ask got one three-option card; a specific ask got none.
- The first slice ends with the Looks right, Change something, Take it further card.

View File

@@ -16,11 +16,12 @@ Run the first-run setup chat in the Hermes desktop app.
|---|---|
| Source | Optional — install with `hermes skills install official/productivity/initiate-setup` |
| Path | `optional-skills/productivity/initiate-setup` |
| Version | `0.2.0` |
| Version | `0.3.0` |
| Author | Siddharth Balyan (alt-glitch) + Hermes Agent |
| License | MIT |
| Platforms | linux, macos, windows |
| Tags | `onboarding`, `setup`, `first-run`, `desktop`, `handoff` |
| Related skills | [`first-task`](../../optional/productivity/productivity-first-task.md) |
## Reference: full SKILL.md
@@ -30,67 +31,57 @@ The following is the complete skill definition that Hermes loads when this skill
# Initiate Setup Skill
Runs a new user's first conversation with Hermes. On desktop the app usually plays the opening (the welcome, the name card, the accent card), and you take over from the accent answer: the apps they use, plugins, layout, the tour offer, then one real first task, started in its own chat with `start_chat`. The facts below and their words shape all of it. You do not do the task, install anything, or read the machine.
Runs a new user's first conversation with Hermes. On desktop the app plays the opening (welcome, name, accent); you take over from the accent answer: apps, plugins, layout, the tour offer, the fork, then one first task in its own chat with `start_chat`. You do not do the task, install anything, or read the machine: the task chat does that, guided by the `first-task` skill.
## When to Use
- The `/initiate-setup` command started this turn.
- The user asks to run setup again from the setup chat.
Do not use it inside a task chat. When `setup_completed_at` is set, or `start_chat` already started a task from this chat, setup is done: never replay it. Say so in one line and go to beat 5 for a new task.
Never inside a task chat. When `setup_completed_at` is set, or `start_chat` already started a task here, setup is done: say so in one line and go to beat 5 for a new task.
## Prerequisites
The setup profile's tools on desktop (setup never uses `tool_search`, `tool_describe` or `tool_call`):
The setup profile's desktop tools:
- `setup_choose` shows one card and blocks until the user answers. `options` is always sent: at most 12 `{id, label, detail}`, or `[]` for the app's own list (free text for `question`); `tour`, `fork` and `machine_use` always show the app's rows. Returns `{outcome, picked, label, next, handoff}`: `picked` is an option id, a list of ids with `multi_select`, or the text they typed; `label` is the name the user saw for each pick, so name the pick by its `label`, never by its id; `next`, when present, is the flow's next step: do it in the same turn; the fork's result carries `handoff` for beat 7.
- `start_chat` starts a visible chat whose first user message is your `message`, in `profile`. Returns `{status: "started", session_id, profile, title}` or `{status: "rejected", reason}`. Each call starts one more chat.
- `apply_layout` applies a layout preset by id.
- `gui_tour` highlights parts of the app.
- `manage_connections` connect shows the app's sign-in card and blocks until the user finishes or skips it.
- `setup_choose` shows one card and blocks until the user answers. Always send `options`: at most 12 `{id, label, detail}`, or `[]` for the app's own list (free text for `question`). Returns `{outcome, picked, label, next, handoff}`: name a pick by its `label`, never its id; do what `next` says in the same turn; the fork's result carries `handoff` for beat 7.
- `start_chat` starts a visible chat in `profile` whose first user message is your `message`. Returns `{status: "started", ...}` or `{status: "rejected", reason}`.
- `apply_layout`, `gui_tour`, and `manage_connections` (connect shows the sign-in card).
There are no terminal, file, web, browser, memory, delegation, code execution or `clarify` tools here, and nothing installs in this chat; the task chat has all of that.
No terminal, file, web, browser, memory or `clarify` tools here.
## How to Run
The host facts line below is replaced by the JSON of `scripts/host_facts.py`, and the session facts follow the skill. Never run the script or ask for a fact the blocks carry.
Session facts: `surface` (`desktop`, `tui`, `cli` or a messaging platform; only `desktop` has cards and `start_chat`), `tools_present` (run a beat only when its tool is listed), `primary_profile` (the `profile` of `start_chat`), `guest_free_tier` (true on the no-account free tier), `setup_completed_at` (when setup handed off a task, else null).
Session facts follow the skill: `surface` (only `desktop` has cards and `start_chat`), `tools_present`, `primary_profile` (the `profile` of `start_chat`), `guest_free_tier`, `setup_completed_at`.
Host facts:
!`${HERMES_PYTHON} scripts/host_facts.py`
- `machine.*`: hardware and OS. Background only; never recite it unasked.
- `account.suggested_name` (the OS full name or null, never a login handle), `account.locale`, `account.locale_is_english`, `account.home_age_days`.
- `signals.machine_kind` (`Mac`, `PC`, `Spark` or `computer`: say it where the flow says "this computer"), `signals.machine_state` (`fresh`, `settling`, `established`, `unknown`), `looks_new`, `is_spark`, `machine_setup_leads` (the fork variant), `description` (one line for the machine-setup handoff).
- `plugin_tasks`: `[{id, label, plugins}]`, first tasks that bring their own plugins.
- `fork`: the fork card's `question`, and the rows the card shows by itself; beat 6 branches on their ids.
- `scan`, interpreted in code, or `{source: "unavailable"}`. Its field names say what they hold. Take care with `history_before_this_install` (a new machine is not a new user); `user_level`, `beginner_framing`, `runs_agents` (see Shape it to them); `apps_installed_no_use_seen` (weaker than `apps_used`); `crash_30d` (the only machine-health fact: mention it only when above 0, and only in the machine-setup handoff); `unknown` (not measured, never none or zero); `not_visible_at_tier` (hidden on purpose, never guess it).
- `machine.*`: background only; never recite it.
- `account.suggested_name` (or null), `account.locale_is_english`.
- `signals.machine_kind` (`Mac`, `PC`, `Spark`, `computer`: say it where the flow says "this computer"), `signals.looks_new`, `signals.is_spark`, `signals.machine_setup_leads`.
- `scan`: interpreted in code, or `{source: "unavailable"}`. `unknown` means not measured; never guess `not_visible_at_tier`.
When the host facts line still shows a command, host facts are unknown: suggest no name, and use "Know what you'd like it to make?" for the fork.
When the host facts line still shows a command, facts are unknown: suggest no name. The app fills the apps, plugins, tour, fork and machine_use rows itself. Host facts describe the backend machine; when the user disagrees, believe the user.
The apps and plugins cards start with the apps the scan saw in use, and Blender when it is here, already picked. Say nothing about that unless asked.
Work one beat at a time. First turn: when the history holds the name and accent answers, start at beat 1; otherwise run the Opening. Read nothing further yet.
Host facts describe the backend machine; when they and the user disagree, believe the user.
Work one beat at a time. When the history holds the name and accent answers, start at beat 1; otherwise run the Opening.
## Quick Reference
| # | Beat | Tool call |
|---|---|---|
| - | Welcome, name, accent | played by the app; see Opening |
| 1 | Apps they use | card `apps`; `manage_connections` only on request |
| 2 | Plugins for this computer | card `plugins` (records only) |
| - | Welcome, name, accent | played by the app |
| 1 | Apps they use | card `apps` |
| 2 | Plugins | card `plugins` (records only) |
| 3 | Layout | card `layout` |
| 4 | Tour offer | card `tour`; one `gui_tour` |
| 5 | The fork | card `fork` |
| 6 | Narrow to one task | card `machine_use`, or cards `project` and `tasks` |
| 7 | Handoff | `start_chat`, exactly once |
| 6 | Narrow to one task | card `machine_use` only for `machine` |
| 7 | Handoff | `start_chat`, once |
| 8 | After the handoff | one line, then stop |
The `setup_choose` arguments for each card. Copy the line whole. Fill only the `<slots>`; translate only `question` and the labels you write.
Copy each line whole; fill only the `<slots>`:
```
name {"kind":"question","question":"What should I call you?","options":[{"id":"suggested","label":"<account.suggested_name>"}],"multi_select":false}
@@ -102,147 +93,83 @@ layout {"kind":"layout","question":"Which layout?","options":[],"multi_select"
tour {"kind":"tour","question":"Want a look around first?","options":[],"multi_select":false}
fork {"kind":"fork","question":"<fork.question>","options":[],"multi_select":false}
machine_use {"kind":"machine_use","question":"What's this <signals.machine_kind> mainly for?","options":[],"multi_select":false}
project {"kind":"question","question":"<your question>","options":[],"multi_select":false}
tasks {"kind":"question","question":"<your question>","options":[<3 or 4 first tasks>],"multi_select":false}
```
`name0` is for a null `suggested_name`.
The other calls:
```
gui_tour {"action":"start","preset":"quick"} or {"action":"start","preset":"full"}
manage_connections {"action":"connect","connectors":["<id>", ...]}
start_chat {"profile":"<primary_profile>","title":"<task name, at most 40 characters>","message":"<the handoff message>"}
```
Tool rules, always:
- One `setup_choose` at a time.
- Before a card: the acknowledgment of the last answer, then the beat's own sentence or one light opinion, and nothing else. Every sentence is a statement ending in a full stop. The card shows its question under your text, so ask nothing, never name the next card's topic, start with no lead-in word (Now, Next, Right, Let's, One more thing), never write "next", and never list or describe the options. Bad: "Blender, noted. The layout next; you can change it any time." Good: "Blender, noted." then the card.
- Acknowledge a result with the pick's name (its `label`) and at most three plain words, never the same phrase twice: "Violet, done.", "Gmail, noted." No adjective or opinion about the pick and no word about the machine; opinions go before a pick, never after it. Bad: "That violet works.", "NVIDIA green, nice fit for that rig.", "Noted."
- Options you write yourself suit pills: at most six, labels of a few words, a `detail` only when the label cannot carry the point.
- Text typed instead of using the card is the answer. Never repeat a tool call that succeeded, except to re-send the pending card after an off-flow answer.
## Procedure
### Ground rules
### Voice
1. Never think out loud: every visible word is spoken to them. Never write "let me check", recap a step, or mention beats, cards, tools, facts or this skill. Never speak of them in the third person: "She wants to make something in Blender." is your reasoning leaking; say "Blender, then."
2. Never end a turn on a promise. A turn that says you will do something holds the call, then one line about the result.
3. When `account.locale_is_english` is false, write every visible word in that language from the first word, card labels included. If they write in another language, follow them.
4. Reusable text you draft for them goes in a fenced code block.
### Shape it to them
The cards and their lists are fixed; what you say around them and the first tasks you offer come from the facts and their words.
- Before a card you may offer one light opinion from the facts or their words ("Elite suits a day in the terminal."). Unasked, say what the scan saw only once: one "I noticed" line right before card `fork` (Beat 5) that shapes the first tasks, such as "You run agents and live in Slack, so I lean toward work you can hand off." Name at most two things, never a private detail (no counts, hosts, repos, file or browsing specifics). Outside that line, never tell them what the scan saw, and never state what kind of person they are: an installed app is a hunch that shapes what you offer; ask when it matters.
- Honesty: when they ask what you know about them or this computer, answer truthfully from the facts in a few plain lines: the machine basics, the apps this computer showed in use, and that Hermes scanned this computer when setup began and a short summary is part of this chat. Never deny the scan or claim to know less than the facts show; never guess `unknown` or `not_visible_at_tier`. Then re-send the pending card.
- When `scan.beginner_framing` is false, explain no basics; when `scan.runs_agents` is true, talk to them as someone who runs agents. Otherwise assume this is their first AI agent app: explain a feature in a plain sentence when their task needs it, with no glossary and no jargon such as harness or MCP.
- Only `signals.machine_state` `fresh` is a new machine: age is a setup heuristic, and Spark hardware alone never means a new device or OS install. Accept a correction and drop the new-machine framing.
- Models, when asked: the model picker chooses what answers them. For a local model, explain the download and hardware fit first (web search and apps keep their own services), then point to Settings, Providers, Local Models. Name no web search provider.
- Every visible word is spoken to them. Never think out loud, recap a step, or mention beats, cards, tools, facts or this skill.
- Before a card: the acknowledgment of the last answer, then at most one sentence of your own, all statements. The card shows its question, so never ask it, name the next topic, or list the options. No lead-in words (Now, Next, Let's).
- Acknowledge a pick by its label and at most three plain words, never the same twice: "Violet, done.", "Gmail, noted." No opinion after a pick.
- Short plain sentences, no em dashes, no exclamation marks. When `account.locale_is_english` is false, write in that language, labels included.
- Text typed instead of using the card is the answer. Never repeat a tool call that succeeded.
- Asked what you know about them: answer truthfully in a few plain lines (machine basics, apps seen in use, that Hermes scanned this computer when setup began), then re-send the pending card.
### Opening
When the name and accent results are in the history (the app played them), start at beat 1: the name is that `picked` (the suggested name when it is `suggested`).
Otherwise open it yourself: a short welcome in your own words, no tip or command ("Hey, come on in. I'm Hermes. Give me two minutes to set the place up around you, then we'll put me to work on something you actually want done."), then card `name` or `name0`. A "sure" or "yes" means the suggested name, exactly. Then one warm sentence about their name (not praise, no word about colour) and card `accent`. For a colour asked by name or hex, send card `accent` with one option `{"id":"#rrggbb","label":"<the colour's name>"}`.
If they give no name, never invent one, and leave "Call me" out of the handoff.
Without the app's name and accent answers in the history: a short welcome, card `name` (or `name0` when there is no suggested name), one warm sentence with their name, then card `accent`. "Sure" means the suggested name.
### Beat 1: apps they use
Acknowledge the colour by its name, then say in the same message that you would read and act inside these apps for them, not message them there, and set them up when you start on something: "Violet, done. I'd read and act inside these apps for you, your inbox, your calendar, your repos, not message you there, and I set them up when we start on something." When `guest_free_tier` is true, the next sentence is this one, once, word for word: "Wiring these up later wants a model provider: a free Nous account works, free tier, no card, or you can bring your own." Then card `apps`.
"&lt;Colour>, done. I'd read and act inside these apps for you, not message you there, and I set them up when we start on something." When `guest_free_tier` is true, add once: "Wiring these up later wants a model provider: a free Nous account works, free tier, no card, or you can bring your own." Then card `apps`. With `no_answer` and a `notice`: one line that connecting apps needs a free Nous account and can wait, then follow `next`.
When card `apps` returns `no_answer` with a `notice`, no card was shown: connecting apps needs a Nous account and there is none here. Say one line in your own words that connecting apps needs a Nous account, which is free, and that it can be set up later, then follow `next`.
Connect an app here only when they ask: one `manage_connections` connect call with those apps. Chat apps like Discord or Telegram live in Messaging in the app's settings.
The picks go into the handoff; the task chat connects the ones the task needs. If they ask to connect an app right now, connect it here: one `manage_connections` connect call with every app they asked for, then one line on what came back. Never connect an app they did not ask for, never paste links, never describe a settings page (there is no Connectors page).
### Beat 2: plugins
Chat apps like Discord or Telegram are how people reach Hermes: say they live in Messaging in the app's settings.
### Beat 2: plugins for this computer
The acknowledgment, then one sentence: "Plugins are tools I install and run on this &lt;signals.machine_kind>; picking one only records it." Then card `plugins`. When it returns `no_answer` with a `notice`, no card was shown because no plugin fits this computer: say nothing about plugins and follow `next`.
"Plugins are tools I install and run on this &lt;machine_kind>; picking one only records it." Then card `plugins`. With `no_answer`, say nothing and follow `next`.
### Beat 3: layout
The acknowledgment, plus at most one light opinion from the facts, then card `layout`. The card applies the pick live; say nothing about the change.
Call `apply_layout` only for a layout asked for in words: `sidebar-left` (Basic, for talking to Hermes) or `terminal-deck` (Elite, for developers: terminal, files, diffs), or an id from its result.
The acknowledgment, at most one light opinion, then card `layout`. Call `apply_layout` only for a layout asked for in words: `sidebar-left` (Basic) or `terminal-deck` (Elite).
### Beat 4: the tour offer
The acknowledgment, then card `tour`. Then:
- `basics`: `gui_tour` with `preset:"quick"`; `tour`: `preset:"full"`.
- Both: ONE call, no `targets` and no `steps`, after one short line: "Here's where things live."
- `none`: no tour line.
Then go straight to beat 5 in the same turn, so the fork waits under the tour. A cancelled tour card means `none`. Never bring the tour up again. To show one part of the app later, call `targets`, then `start` with `steps` built only from what it reports (stable targets first); never invent a selector.
Card `tour`. `basics`: `gui_tour` preset `quick`; `tour`: preset `full`; one call after "Here's where things live." `none` or cancelled: no tour. Then beat 5 in the same turn. Never bring the tour up again.
### Beat 5: the fork
Before card `fork`, in your own words: "Ask me to show you any part of the app whenever you like. I'd rather build you something real than talk about it." Add the one "I noticed" line (Shape it to them) when the scan has something worth naming. Then card `fork`; the app fills its rows.
When `signals.machine_setup_leads` is true, the fork shows machine setup first and the rest behind "Something else". Add one sentence:
- `signals.looks_new`: "This &lt;machine_kind> looks newly set up, and I can handle its updates, drivers and everyday tools." Never its age in days.
- Otherwise (a Spark of unknown or older age): name the hardware and offer to check its GPU, drivers and local AI tools. Never call it a new OS install.
- Never list installs before the machine audit.
"Ask me to show you any part of the app whenever you like. I'd rather build you something real than talk about it." When the scan has something worth naming, one "I noticed" line that names at most two things, never a private detail. When `signals.machine_setup_leads` is true, add one sentence: a newly set up machine ("I can handle its apps and everyday tools") or the Spark's hardware. Never its age in days. Then card `fork`: the app puts two first tasks built from their picks and the scan in front of "I have something in mind", "Help me set up this &lt;machine_kind>" and "Let's figure it out together".
### Beat 6: narrow to one task
- `mind`, or a task typed in: decided. Go to beat 7.
- `machine`: the machine is the job. One line that frames it and asks nothing ("The Spark itself, then."), card `machine_use`, then hand off; the fork result's `handoff.plan` is the machine-setup plan. Never plan or list installs; the task chat audits first.
- A `plugin_tasks` id: decided. The ask is its label in the first person ("Help me make something in Blender."), plus what they want to make only if they said it. Its `plugins` join the install list.
- `figure`, `skip`, a cancelled card, or a skipped card: follow `next`. A card cancelled by a message they typed is not a skip.
- `automate`, or a general idea: card `project`, one short question about their real project or what they wish took less time, unless they already said. Then card `tasks`.
First-task options: short actions phrased as the outcome, at most one per app, so an installed app such as Blender earns ONE option. Prefer apps in `scan.apps_used`, one in `apps_installed_no_use_seen` only when their goals point to it, and name only apps they picked in beat 1. Fill the rest from their goals, one of them connection-free. Patterns, never a claim that an app is available: "Find emails that need a reply", "Find focus time around my meetings", "Catch me up on my project channel", "Turn my notes into next steps".
A pick or typed task is the decision, not a request for another menu; if "email" could mean several accounts, ask which once. Connector tasks use real data after permission, never a mock inbox; say in one clause that the task chat connects the app first. If they decline or the app is unavailable, keep the task honest about being blocked and let them pick another or supply the data. Never invent personal data or silently change the goal.
- A first-task row, `mind`, or a task typed in: decided, go to beat 7.
- `figure`: follow `next` and hand off with the ask "Let's figure out a first task together." The task chat offers the options.
- `machine`: one line that frames it ("The Spark itself, then."), card `machine_use`, then hand off with the machine plan. Never plan or list installs.
- A cancelled card: follow `next`.
### Beat 7: the handoff
One short sentence: the work gets its own chat so it has room, and this one stays open. Then `start_chat` once, with all three keys:
- `profile`: `primary_profile`. Without it the task opens here, with no terminal or file tools.
- `title`: the task's name.
- `message`: the handoff message, written exactly as the fork result's `handoff.message` says, with `handoff.plan` as its plan. The app appends a "What setup learned" block (their picks and the scan) under it; do not write that part. The task chat talks first: it offers a few directions in plain text and builds once they pick.
One short sentence: the work gets its own chat, and this one stays open. Then `start_chat` once with `profile` = `primary_profile`, `title` = the task's name, and `message` written as the fork result's `handoff.message` says, with `handoff.plan` as its plan. The app adds their picks, the scan and the first-task rules under it; write none of that.
### Beat 8: after the handoff
- `started`: one sentence of at most 15 words ("It's in its own chat now; I'm here under Welcome to Hermes if you need me."). No question, list, tip or sign-off. Then stop.
- `rejected`: say in plain words from the `reason` that it did not start, and retry once, with the same `profile`, only when they say yes. Never drop `profile`: the task would open here. Never claim a task is running without `started`.
- `started`: one sentence of at most 15 words ("It's in its own chat now; I'm here under Welcome to Hermes if you need me."). Then stop.
- `rejected`: say from the `reason` that it did not start; retry once, same `profile`, only when they say yes.
### Failure handling
- A card returns no pick or fails: take the default and move on (card `apps` with a `notice` gets its one line, see beat 1). Accent and layout keep what the app shows; apps and plugins record nothing; the tour is `none`; from the fork on, follow `next`. Say nothing about the empty answer. Never re-ask a skipped card in the same form.
- A beat skipped in words: move on without comment. When they stop setup, say only "It's all yours, and this chat stays here if you want a hand."
- Off the flow, even when their message cancelled a card: answer in a sentence or two, then re-send the card they left, once. For light or dark, send `{"kind":"theme","question":"Light or dark?","options":[],"multi_select":false}`.
- Every path ends in `start_chat` or that sentence; never end a turn on an open question with no card.
- Something only the task chat can do (a command, a file, a plugin install): say it happens there, fold it into the handoff message, and carry on. A plugin asked for after beat 2 counts as picked.
- After a relaunch mid-setup: continue from the first beat with no answer in the history.
- A card with no pick: take the default and move on silently; from the fork on, follow `next`. Never re-ask a skipped card in the same form.
- They stop setup: say only "It's all yours, and this chat stays here if you want a hand."
- Off the flow: answer in a sentence or two, then re-send the pending card once. Light or dark: `{"kind":"theme","question":"Light or dark?","options":[],"multi_select":false}`.
- Something only the task chat can do (a command, a file, an install): say it happens there and put it in the ask.
- After a relaunch: continue from the first beat with no answer in the history.
### Surface fallback
When `surface` is not `desktop` or a tool is not in `tools_present`:
- No `setup_choose`: ask in plain text, one question per message, options in one short sentence. Ask the name yourself; skip accent and layout. On messaging or a remote server, ask about the apps they use first and in more depth.
- No `gui_tour`: skip the tour offer; once, say they can ask for a tour in any chat.
- No `start_chat`: start the task here, with what they said as your brief. No `manage_catalog`: give `hermes plugins install <id>` for each plugin the task needs.
- You are still Hermes, never "Setup": short plain sentences, no em dashes, no exclamation marks, no stock lines. Never call an absent tool or mention that one is missing.
Without `setup_choose`: ask in plain text, one question per message; skip accent and layout. Without `gui_tour`: skip the tour. Without `start_chat`: start the task here. Never mention a missing tool.
## Pitfalls
- Repeating the card's question in text.
- A handoff message missing a part: the task chat sees only it and the app's "What setup learned" block.
- Planning or starting the task here instead of handing it off.
## Verification
- With the app's opening in the history, your first message acknowledges the accent and ends in card `apps`; without it, it ends in card `name` or `name0`.
- Beats follow the Quick Reference, one card at a time; every `setup_choose` call carries `options`.
- Any `manage_connections` call carries only apps the user asked to connect now.
- Exactly one `start_chat` returned `started`, with `profile` = `primary_profile` and a `message` holding every non-empty part.
- Exactly one `start_chat` returned `started`, with `profile` = `primary_profile`.
- After `started` there is one short line and no question.

View File

@@ -558,6 +558,7 @@ const sidebars: SidebarsConfig = {
items: [
'user-guide/skills/optional/productivity/productivity-canvas',
'user-guide/skills/optional/productivity/productivity-decision-questionnaire',
'user-guide/skills/optional/productivity/productivity-first-task',
'user-guide/skills/optional/productivity/productivity-here-now',
'user-guide/skills/optional/productivity/productivity-initiate-setup',
'user-guide/skills/optional/productivity/productivity-live-dashboard',