Skip to content

feat(skills): tutorial — voice walkthrough + AG2 Space Getting started page - #5597

Merged
sonichi merged 17 commits into
mainfrom
feat/tutorial-skill-getting-started-page
Oct 16, 2026
Merged

sonichi merged 17 commits into
mainfrom
feat/tutorial-skill-getting-started-page

Conversation

@sonichi

@sonichi sonichi commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

What changed, and why?

A new tutorial skill (owner ask, the owner). It has two modes:

  • Voice mode: the existing walkthrough, moved out of docs/tutorial-delivery.md into the skill without changing it. It reads notes/first-time-tutorial.md and delivers one short section at a time. One sentence is new: a section that needs an app the owner hasn't connected gets one line about connecting it, not the full step.
  • AG2 Space mode: right after the first-contact greeting in the owner's DM, scripts/getting_started.py publish creates one "Getting started" HTML page (Room Commons) in that DM and summons the owner to it. The only variables on the page are the agent's own name (escaped) and the core type, so nothing from the owner's accounts can reach it.

Page outline (approved by the owner, revision 2). The page is a short AG2 Space tour. Each part is one line on what it is, plus one thing to try: a Copy-prompt card where a prompt makes sense, otherwise a short how-to line.

  1. Rooms: humans and agents in one chat. Try: create a room with the + next to Rooms, then invite the agent.
  2. Commons: the room's shared Doc, HTML pages, whiteboard, sheets and databases. Copy: "Make a plan for our team offsite in Commons."
  3. Teamwork: @-mention any agent to hand it work; it replies in the thread. Copy: a research ask that ends with handing the review to another agent in the room.
  4. Calls & voice: talk live in any room or the DM; agents join the call. How-to: the call button in the room header (Start call).
  5. Connect other agents: + → Agent opens Add an agent → Use an agent from another app → Dot (in ChatGPT), Grok Bot (in Grok), Muse, or Other (any app with connectors).
  6. Connect your apps: Gmail, Calendar, Drive, Slack… via Settings → Integrations. Copy: "Connect my Google Calendar."
  7. Activity & notices: the room's Activity tab shows what needs you and what your agents are working on. The unread badge follows the room's notification setting (All Messages, Mention & Keywords or Mute), which can be changed per room.
  8. Mobile: the same rooms and agents on your phone, paired in Settings → Mobile pairing.
  9. Import your Claude Code history: shown only when the core is Claude Code. Copy: "Import my Claude Code history."

the owner's decisions reflected here:

  • Calls and voice are merged into one part.
  • Claude Code and Codex are not listed as addable agents. They are Sutando cores chosen at setup, and a test pins their absence from part 5.
  • The import part is conditional. --core claude|codex|unknown|auto controls it, and publish defaults to auto. Auto reads sutando_config.resolve_core_runtime(), the same resolver behind scripts/sutando-config.sh core-runtime: it takes the launcher's SUTANDO_CORE_RUNTIME stamp, otherwise core.runtime from config. If resolution fails, the core is unknown and the part is omitted.
  • The old separate five-prompt list is gone. Its research prompt now lives inside Teamwork.

UI labels were checked against the web client origin/main @ 2ebcc49f:

  • + menu: Direct Message / Room / Agent (pages/client/home/Home.tsx, AddAgentMenuItem.tsx). The Rooms category has a "Create room" + button.
  • Add an agent dialog: title "Add an agent" and card "Use an agent from another app" (ag2/agentFlowCopy.ts). App agents: Dot, Grok Bot ("in Grok"), Muse, and Other ("any app with connectors") in ag2/agentFlowFaces.tsx. "In ChatGPT, open a chat with Dot" is in ag2/appSetupCopy.ts.
  • Settings: "Integrations" under Apps, and "Mobile pairing" (features/settings/settingsNav.ts).
  • Room header: the call item, with tooltip "Start call" (features/room/header/roomHeaderItems.ts, RoomViewHeader.tsx), and the Commons item.
  • Room views: the Activity tab and its "Needs you" group (RoomViewSwitcher.tsx, room-activity/RoomActivityView.tsx).
  • Notification modes: All Messages, Mention & Keywords, Mute (components/RoomNotificationSwitcher.tsx).

Revision 3: interactive and graphic (the owner), plus two fixes from the live run. The coordinator published revision 2 in the owner's DM (the page). All 9 sections landed, and Copy works in the desktop app.

Page (skills/tutorial/scripts/tour_page.py, pure; the content and wording are unchanged):

  • Graphics. A hero with the agent's name and an inline-SVG illustration, plus one inline-SVG icon per section: chat bubbles with a person and a bot, doc/board/sheet tiles, two agents handing off, a phone with a waveform, linked nodes, app tiles, a bell with a badge, a phone, and an arrow into a folder.
  • Accents. Each section has its own accent colour from a 9-colour palette. A test checks every accent at WCAG AA (4.5:1) as text on the card colour, in both themes.
  • Tour controls. The page opens as a step-by-step tour: numbered step dots (aria-current, done state), Back/Next, a "Show all" toggle (aria-pressed), and the ←/→ keys. Without scripts every section shows.
  • Progress. Each section has a "a teammate done" checkbox, and a progress bar (role=progressbar) reads "3 of 9 done".
  • Persistence. Progress is stored in the HTML surface's page runtime, window.artifact.state (the web client htmlRuntime.ts RUNTIME_SHIM; room-commons SKILL.md "Pages that remember"). The key is gs-done-<artifact.me.id>, so it is per viewer, and artifact.state.on syncs it live. That state is saved with the page, so it survives reloads and a refresh in place. Per room-commons SKILL.md the page has no storage of its own (srcdoc frame, allow-scripts only). The fallback order is therefore localStorage, then memory, and every storage access sits in a try (a test checks this structurally).
  • Unchanged behaviour. Small animations, switched off under prefers-reduced-motion. Narrow layout below 520px. Dark mode. No external URL, no src=, no <img>/<link>, no url(. The Copy fallback is the same as before.

Fixes from the live run (getting_started.py):

  1. ag2.whoami returned display_name: null. --name auto now resolves the name itself and never fails. It tries, in order:

    • the agent's Matrix profile displayname: .well-known/matrix/client → /_matrix/client/v3/profile/<mxid>/displayname, https only, with a User-Agent, because the homeserver's edge returns 403 to urllib's default UA;
    • then the mxid localpart (@chi-air-blue.agent:… → "the owner Air Blue");
    • then "Sutando".

    The mxid comes from $AGENT_MXID, else the channel env file, else --agent-mxid. I found no room.members.list or profile action reachable from this script, so it uses the public profile endpoint. SKILL.md step 1 documents the order. On this host it resolves to "the agent's profile name".

  2. "room-commons: no access token" until the lane env was sourced. room_commons.py looks for <workspace>/.claude-sutando/channels/ag2space/relay-client.env. On this install that directory holds only .env, a link to the app's channel env, so its fallback never fires. publish now passes room_commons.py its own credential variables (read from its TOKEN_VARS/URL_VARS), taken from the channel env file through the shared resolver (channel_env_resolve → channel_token). It does this only when the environment names none of them, so a token is never mixed with a URL from elsewhere. The policy was already implemented privately in pending_questions_room_db.channel_credentials. It is now src/channel_credentials.py, and pending-questions delegates to it; its 15 tests still pass. Nothing prints the token, and a test checks the failure output.

  3. The default --room-commons resolved to the clone. It is now skills/room-commons beside this skill, else claude_home_path("skills", "room-commons"), the installed skill (a symlink into the engine checkout).

Revision 4: review fixes (a reviewer agent, a reviewer agent) (commit 72238b4):

  • a teammate done never writes under a shared key. The per-viewer key is resolved, and the checkboxes stay disabled, until progress has loaded. With no viewer id, progress stays in this browser's localStorage.
  • A thenable state.get is awaited.
  • In Show all, a step dot scrolls its step into view and focuses it (no smooth scroll under reduced motion).
  • The Show all toggle keeps one label; aria-pressed carries the state.
  • The copy fallback names the platform's keys (⌘C, Ctrl+C, or Ctrl/⌘+C).
  • Copy outcomes go to an aria-live status region.
  • No SVG pairs role="img" with aria-hidden.
  • A head script adds .js so a scripted page opens on one step with no flash; without scripts every step shows.

Revision 5: copy, screenshots and size (all approved by the owner) (commits 8c39f34, 0f0f80b):

  • Steps. New order: Your DM, Rooms, Commons, Connect other agents, Teamwork, Calls & voice, Connect your apps, Activity & notices, Mobile, plus the import step on a Claude Code core. That is 10 steps, or 9 without the import.

    • Your DM: answers to the owner's own asks come back in the DM, never in a room. Copy prompt: "What can you do for me today?".
  • Paste hints. Every prompt says where to paste it: "Paste in: your DM" or "Paste in: a room". Commons and Teamwork go to a room.

  • Wording checked against the web client origin/main @ 2ebcc49f:

    • Connect other agents: the picker's own descriptors, Dot (by OpenAI, in ChatGPT), Grok Bot (in Grok) and Muse (by Meta), from agentFlowFaces.tsx and appSetupCopy.ts.
    • Activity & notices: a try-it that ends with "Mention & Keywords" (RoomNotificationSwitcher.tsx). The bell is the "Activity" button at the top of the room list (HeaderActivityButton.tsx).
    • Calls & voice: only the headphone icon in the room header (Icons.Headphone, "Start call"). In your DM the agent answers. In a room you pick it under "Bring an agent?" (callPicker.ts: agents do not join room calls on their own). The user-card microphone is not mentioned or shown, per the owner; a test pins that.
    • Import: starts "If you use Claude Code" and says it reads the local history.
  • Labels reconciled with the screenshots: + at the top of the room list (Home.tsx aria-label="Add") then Room; the room menu (⋮) then Invite; the plug in the user card to Settings → Integrations (SidebarUserButton.tsx).

  • Tour behaviour. When every step is done, a closing card appears: "You're set — ask anything", with a copy prompt. a teammate done moves to the next open step in step mode, not in Show all. The step's title replaces "Step N of M". Section.images lets any step carry screenshots.

  • Screenshots, adapted from a reviewer agent's "Getting started with AG2 Space" page. There are eleven crops in skills/tutorial/assets/: 140 KB in total, each at most 34 KB. Each is cropped to the control or region so no personal room, name, avatar or account shows. They are inlined as data: URIs, so the page is still self-contained.

    Step Images (source shot)
    Rooms + with red box (03), New Chat Room dialog (03), room menu → Invite (04)
    Commons header icons with a red box on Commons (06), surface list (07)
    Teamwork composer @ button (05)
    Calls & voice headphone icon (09)
    Connect your apps plug icon (00), Settings → Apps → Integrations (01)
    Activity & notices bell (10)
    Mobile Mobile pairing panel (12)

    Dropped:

    • 02 (microphone): the control is being removed.
    • 08 (Commons beside chat): the page list and doc show personal names.
    • 11, 13, 14 (marketplace, model, room settings): no step needs them.

    The other part of 05 (its mention list), the rest of 01 (its connected accounts) and the room lists in 03 and 04 were cropped away.

  • Size. The page is now 215,197 bytes with the claude core (215,281 with --audience room), over Room Commons' 196,608-byte single write. publish appends in pieces of at most 120,000 bytes, split on character boundaries. Each piece is one argv string, so it stays under Linux's 131,072-byte per-argument cap; CI caught 150,000 as E2BIG (fixed in a6d732d). A refresh is one replace when both texts fit one argument. Otherwise it clears the old text in pieces (replace <piece> "") and appends the new, so a viewer may briefly see a partial page during a refresh.

Revision 6: second review (a reviewer agent, a reviewer agent) (commits 8698807, 452fa11, plus the asset-tracking fix below).

  • Asset tracking. The repo's .gitignore drops assets/ except skills/*/assets/. So the new Rooms crop, kept under scripts/assets/, was never committed, and CI failed with FileNotFoundError. The screenshots now live in skills/tutorial/assets/, and the one line that walks to the skill root carries the lint's line-scoped allow-repo-root pragma. I ran the test file from a clean worktree at HEAD to prove every asset is tracked. Labels re-verified on the web client origin/main @ 5e6b74b7.
  • Images.
    • Rooms now has its own crop, room-list-add.jpg, with the red box on the +. It previously reused the Activity bell.
    • The Commons header crop gets a 4 px red box on its icon, drawn with Pillow to match the other shots.
    • A test refuses two steps that share identical image bytes.
  • Without JS, progress and a teammate done are hidden rather than shown as stuck, disabled checkboxes.
  • Saving. await art.state.set(...) sits inside a try.
  • Theme. The web client passes no theme into an HTML page today: room-collab/html has no theme hook, and RoomHtmlView.css.ts keeps the frame white "whatever the client's theme". So the page follows prefers-color-scheme, and an explicit data-theme="dark|light" wins: set on <html>, or copied from <body>.
  • Screen readers. A step change is announced once, by focusing its heading; the step title has no aria-live. A repeat copy is re-announced, because the status region is cleared and then set.
  • Arrow keys act only when focus is inside the tour, and not on an input, button, link, select or contenteditable.
  • Show all has a visible pressed style.
  • Position. "3/10" sits beside the step title, aria-hidden because the dots already announce position.
  • Copy.
    • Commons: "Docs, HTML pages, PDFs, whiteboards, sheets and databases".
    • Calls: "start the call and pick your agent under “Bring an agent?”" (CallParticipantPicker.tsx).
    • Activity: "open the room menu (⋮), choose Notifications, and pick Mention & Keywords". The RoomNotificationModeSwitcher "Notifications" item is in the room header's ⋮ menu (RoomViewHeader.tsx) and the room list item's menu (RoomNavItem.tsx).
    • Apps alt text: "Settings → Apps → Integrations".
  • Audience. --audience room (render and publish) says "your agent" throughout, for any room that is not the owner's DM. The default dm keeps the agent's name. The DM step now reads correctly in both.

Revision 7: create once, even across crashes and lost answers (a reviewer's Codex, P2) (commit a22b0fe):

  • The bug. Page-add and every append ran before the marker was written. A marker write that failed, a process that died in that window, or an append failure whose cleanup also failed left a page with no marker entry, so the next run created another page. The old cleanup message also claimed the page was removed when the removal had failed.
  • Creating the page. It is now made with the room Action room.commons.pages.create. I described it live in the owner's DM (a read-only room.actions.describe): idempotency_requirement: required, and its description says "a retry with the same one [operation_id], after a success or a failure, creates the page and writes its body at most once". It takes title, kind: html and a first body of up to 196,608 bytes, and it returns kind. It is called through room-commons' own Actions client (room_commons.mcp_door → commons_actions), not hand-written HTTP.
  • The marker entry moves through three states. It records creating plus a fresh operation id before the call. After the call it records pending plus the kind. Once every remaining chunk is appended it records complete. A rerun resumes from whatever state it finds: creating re-issues the create with the same operation id, and pending rewrites that same page.
  • Ownership. The create's first body starts with <meta name="sutando-tutorial" content="<agent mxid>">, so chunk 1 always carries it. If the marker file is lost, a listed "Getting started" page is reused only when its text carries this agent's marker and the listing's writer (by) is this agent or blank.
    • An empty page, or an unrelated page with the same title, is never adopted or touched.
    • Two marked pages, or no agent id to check with, stop the run with exit 4 and a reason that names the kinds involved.
  • No more cleanup, and no claims about it. A failed append leaves the page pending with an accurate message, and the next run finishes it.
  • Recovery tests (a fake pages.create server that keeps one page per operation id, plus the stub room_commons.py; no network), all passing:
    • test_ambiguous_create_is_retried_with_the_same_operation_id: the server creates the page and the client loses the answer. The rerun uses the same id, and there is exactly 1 page with the full content.
    • test_create_refused_before_landing_is_retried_with_the_same_operation_id
    • test_resumed_create_with_changed_content_rewrites_the_first_body
    • test_crash_after_create_before_recording_its_kind: the pending write fails. The rerun uses the same id, and there is 1 page.
    • test_marker_write_failure_after_the_page_is_whole
    • test_lost_marker_recovers_the_page_by_its_ownership_marker
    • test_failed_append_stays_pending_and_the_next_run_finishes_it
    • test_a_create_answer_without_an_html_kind_is_refused
    • test_unrelated_same_title_pages_are_never_adopted_or_touched (control: another agent's page and an empty page by this agent, both left alone)
    • test_two_tutorial_pages_stop_with_a_reason
    • test_unknown_agent_id_with_a_candidate_stops
    • test_ownership_needs_this_agents_marker

Revision 8: prompts tailored to the owner's work (the owner's decision) (commit f1ac2ba):

  • No pre-written prompt sets per role. When the agent knows the owner's work, it writes the prompts itself at publish time. It knows from the onboarding roles in the first-contact message (a line like "I picked: researcher, founder"; the client's exact wording is still to be decided) or from user_profile.md. SKILL.md step 2 says how: 4–6 prompts, first person, practical, one line, at most 200 characters, runnable on a fresh install (only the apps slot may assume a connected app).
  • --prompts <json> (render and publish) is an object keyed by slot: dm, commons, teamwork, apps, import, finale.
    • Each entry is validated: a string, one line, 1–200 characters, no :// or www., no < or >. It is escaped on render as before.
    • An invalid entry falls back to that slot's generic prompt. A missing, unreadable or non-object file means every slot is generic. Prompts never fail a publish.
    • The slot, not the text, decides the "Paste in: your DM / a room" hint.
  • --roles takes the payload of the first-contact line that the producer, the web client first-contact change, sends: I picked: <Label1>, <Label2>.. These are persona labels (multi-word ones like "Product Manager" included), or slugs when the client had no labels. The payload is passed verbatim, e.g. --roles "Founder, Product Manager.".
    • The payload is split on commas, and each entry is trimmed of whitespace and one trailing period.
    • An entry is kept if it is 1–40 characters of letters, digits, spaces and & - _ ' ., with at least one letter or digit.
    • A slug-looking entry is title-cased (product-manager becomes "Product Manager"); a label is kept as written.
    • Duplicates are dropped case-insensitively, at most 6 are shown, and they are escaped.
    • The result is a "Picked for: Founder, Sales" line under the heading, with no server call. Commit 16fafab fixed --roles "Founder, Sales.", which had shown only "Founder".
  • Tests:
    • a valid file overrides its slots and is escaped;
    • each invalid kind (URL, too long, multi-line, non-string, markup, blank, unknown slot) falls back per slot;
    • broken JSON, non-object JSON or a missing file gives the generic page;
    • the paste hint is unchanged by the prompt text;
    • the roles line takes the I picked: payload as given: "Founder, Sales." gives both entries, "Product Manager" keeps its space, slugs founder,sales give "Founder, Sales", and junk, markup and over-long entries are dropped. It is also de-duplicated, capped at 6, and escaped (R&D renders as R&amp;D);
    • the CLI passes both flags through, for render (subprocess) and publish (in-process).

Review nits (a reviewer, approval at 70617b2) (commit after f1ac2ba):

  1. Fixed. SKILL.md now says 120,000-byte pieces (CHUNK_BYTES), because Linux caps one argv string at 131,072 bytes. A test (DocTest) fails if SKILL.md names any other piece size.
  2. Already fixed in a22b0fe. A lost marker no longer creates a second page: the run reconciles from the page listing. It deliberately does not trust the title alone (per a reviewer's review); a page is reused only when its text carries this agent's ownership marker.
  3. Not done here. CI does not install jsdom, so PageBehaviourTest skips there; it runs locally with NODE_PATH. Running it in CI needs a devDependency and a workflow step, which is a repo-wide CI change; I'd rather do that as a follow-up than bundle it into this PR.
  4. Fixed. SKILL.md documents --audience room: use it only when the owner asks for the page in another room, and summon only whoever they asked for.
  5. Strengthened. room_commons.py errors are now redacted of every credential-like value in the environment, including each half of a url|secret token. The test's stub now leaks its own token, so the test exercises the redaction rather than our own echo.

Re-review fixes (a reviewer, REQUEST_CHANGES at 6949462) (commit 1a1244a):

  • Blocker, fixed. create_page put the Actions client's exception into the publish reason unredacted. It now goes through redact(), as does the bad-kind message, and main() redacts every failure reason it prints.
    • Audit: every place a room-commons or Actions-client output becomes a reason now goes through redact(): Commons.run, which also feeds summon_error; create_page; and main()'s exit-4 JSON. The remaining messages carry only page kinds, file paths, or our own validation text.
    • Tests: an Actions client that raises with Bearer … and a url|secret relay token leaves neither in the error or in main()'s stdout JSON.
  • Nit 1, fixed. --roles strips a leading I picked: (any case, optional whitespace), so the whole line can be passed. Tested in both forms.
  • Nit 2, fixed. Resuming a creating entry first looks for a listed page tagged by this agent and finishes it. A create is re-issued with the same operation id only when no such page is listed, so it no longer relies on the server remembering the id. Tested both ways.
  • Nit 3, fixed. redact() also masks anything shaped like Bearer <token> or https://…|<secret>, not only values taken from the environment.
  • Nit 4, partly fixed. Prompts now refuse bare domains such as example.com. "First person" and "runs on a fresh install" stay SKILL.md rules for the agent, because a regex can't check them.
  • Nit 5, deferred. A live round trip of a successful create retried with the same operation id (the server's dedupe) is not in the receipts. The live receipt covers a refused create retried with the same id. Proving success-dedupe live needs a create whose answer is lost after it lands, which can't be arranged from the client. The code no longer depends on it: nit 2's tagged-page check finishes the landed page instead.
  • Follow-up (from the first review, nit 3): run PageBehaviourTest in CI by installing jsdom there.

Create once. <workspace>/state/tutorial/getting-started.json records the page kind for each room, and writes to it are atomic and under a lock. Re-running in the same room rewrites the page if its content changed (including a core change, which adds or drops the import part), or does nothing if it didn't. A re-link or reinstall that reuses the DM therefore refreshes the same page instead of adding a second one. If someone removed the recorded page from the list, it is created again. The owner is summoned only when the page is created, or on a refresh run with --summon.

The greeting is never blocked. The skill runs after the reply has been sent. Exit 3 means room-commons is not installed. Exit 4 means a room-commons call failed: the marker is left unchanged, and an empty page this run added is removed again. In both cases the greeting stands and nothing more is said. A summon failure still reports the page (summon_error).

How the Copy prompts work. The HTML surface renders pages in an allow-scripts-only sandboxed srcdoc frame. It exposes window.artifact (state, events, db) and link forwarding, but it has no hook for inserting text into the composer: see the web client htmlRuntime.ts, htmlLinks.ts and htmlSurface.ts. I did not invent one. Each button tries navigator.clipboard.writeText, then falls back to execCommand('copy'). If both are refused, it selects the prompt text and the button reads "Press ⌘C".

The wiring at first contact goes in a separate PR in the desktop app, which owns engine/CLAUDE.md / engine/AGENTS.md: (link in a comment below). Until this skill ships in the engine, that bullet's "if the skill is missing" clause applies.

Related: #3924 (draft /wizard tour: a local card plus text, no page) is a separate surface. This PR does not touch it.

What should reviewers look at first?

  1. skills/tutorial/scripts/getting_started.py: plan() / publish() (the idempotency marker and the room-commons call sequence).
  2. skills/tutorial/SKILL.md: the ordering rule (greeting first, never retracted).
  3. tests/tutorial-getting-started.test.py.

What user behavior does this prove?

A new owner gets the greeting, and then one Getting started page in their DM: an AG2 Space tour with one thing to try in each part. A re-link does not create a duplicate page.

Feature/documentation consistency

docs/tutorial-delivery.md (canonical for the tutorial) now points to the skill and describes both modes. Its docs/catalog.json entry has a new canonical_for and last_verified: 2026-10-15, and the docs/README.md line is updated. skills/room-commons is shipped by the desktop app as an overlay and is not in this repo, so the skill finds it at skills/room-commons/scripts/room_commons.py, with a --room-commons override.

Before/after evidence

Parent commit (skill absent):

$ python3 tests/tutorial-getting-started.test.py
FileNotFoundError: [Errno 2] No such file or directory: '.../skills/tutorial/scripts/getting_started.py'

HEAD:

$ python3 tests/tutorial-getting-started.test.py
Ran 111 tests (103 without jsdom; the jsdom class skips unless node can load jsdom)
OK

Positive controls:

  • Mutating plan() to always return create made 3 tests fail.

  • Mutating sections_for() to show the import part on any non-Codex core made 1 test fail.

  • Dropping the try around art.state.set failed the storage-guard test.

  • Adding an https:// icon link failed the no-external-URL test.

All four mutations were reverted afterwards.

The page's JS was run in jsdom (from the web client's node_modules, locally, not in CI) in two modes:

no runtime, storage throws: errors=[] mode=step progress="0 of 9 done" dots=9 Next: Rooms→Commons, mark → "1 of 9 done", Show all → mode=all
page runtime (fake artifact, me.id=u42, rooms done): resumes at Commons, "1 of 9 done" → mark → "2 of 9 done", saved gs-done-u42=["rooms","teamwork"]

I also took headless Chrome screenshots at 360px and 760px widths, light and dark: no horizontal overflow, and the accents are legible.

Rendered parts with render --name Blue:

--core claude: Rooms | Commons | Teamwork | Calls & voice | Connect other agents | Connect your apps | Activity & notices | Mobile | Import your Claude Code history   (5492 bytes, 4 Copy prompts)
--core codex:  Rooms | Commons | Teamwork | Calls & voice | Connect other agents | Connect your apps | Activity & notices | Mobile   (5119 bytes, 3 Copy prompts)

The argv the script sends was parse-checked against the shipped room_commons.py (--help on each form: --json pages, --json page-add, --json --kind html-x read, --kind html-x append|replace|summon, page-remove). All forms parse. The page's inline script passes node --check.

What tests did you run?

$ python3 tests/tutorial-getting-started.test.py          # 103 OK, 1 class skipped (no jsdom)
$ NODE_PATH=<the web client>/node_modules python3 tests/tutorial-getting-started.test.py   # 111 OK
$ coverage run (this file + pending-questions-room-db-serve-process) && diff-cover coverage.xml --compare-branch=origin/main --fail-under=95
Diff Coverage: getting_started.py 100%, tour_page.py 100%, channel_credentials.py 100%, pending_questions_room_db.py 100% — Total 489 lines, Missing 0, Coverage 100%
$ python3 tests/pending-questions-room-db-serve-process.test.py   # 15 tests OK (delegation)
$ python3 scripts/gen-src-map.py --check                  # up to date (adds channel_credentials.py)
$ bash scripts/lint-workspace-resolution.sh               # clean
$ python3 scripts/lint-skill.py --all --strict            # 63 manifest(s), 0 error(s), 0 warning(s)
$ ruff check skills/tutorial tests/tutorial-getting-started.test.py   # All checks passed!
$ python3 skills/release/scripts/docs_audit.py            # docs-audit: PASS (53 indexed docs)
$ python3 tests/release-docs-audit.test.py                # ALL PASS (5 checks)
$ bash scripts/review-checks.sh --diff <(git diff origin/main...HEAD)
review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)

The tests cover:

  • Interactive (rev 3): tour controls and progress markup; 9 a teammate done boxes; an SVG per section plus the hero; no external URLs for any core; dark mode, reduced-motion and narrow-width blocks; every storage access inside a try; per-viewer artifact.state key; accent palette at AA in both themes; both page scripts pass node --check (skipped when node is absent).

  • Name (rev 3): explicit name kept; auto uses the profile displayname from the well-known homeserver; then localpart, then Sutando; a non-https homeserver is not followed.

  • Credentials (rev 3): names read from the script; the env file fills them; an env credential wins with nothing mixed in; the token is never printed; the installed room-commons is found.

  • Rev 4/5:

    • Page markup and storage: shared key never written without a viewer id; a thenable get; dots scroll and focus in Show all; one stable toggle label; aria-live; no role=img with aria-hidden; the .js class set in the head.
    • Tour content: DM step first; Connect other agents before Teamwork; paste hints; closing card; a teammate done auto-advance (and none in Show all); step title; reviewed wording; no microphone.
    • Screenshots: assets exist and are small; each step's images are inlined; no external URL beside the data: images; page size bound.
    • Publish: large pages chunked on create and refresh; a small change is one replace.

    Each was checked by mutation: auto-advance removed, chunking removed, the shared-key fallback, the dot scroll.

  • Content: all eight parts in order for every core; the import part present only with claude and absent for codex and unknown; a bad core value is refused; one Copy button per prompt, with the connect prompt kept; dark mode; Codex and Claude never offered in Connect other agents; rendering is deterministic; the name is escaped everywhere; a bad name is refused.

  • Marker: survives a corrupt file, round-trips, and drives plan.

  • publish, against a stub room_commons.py:

    • first run creates, writes and summons;
    • rerun is unchanged with no summon;
    • a content change or a core change rewrites the same page;
    • a removed page is created anew;
    • a failed write removes the empty page and leaves no marker;
    • a failed summon still reports the page.
  • CLI: render for each core; detect_core() follows the SUTANDO_CORE_RUNTIME stamp; exit codes 3 and 2.

Live path?

Live receipts. The coordinator ran these against real rooms with this skill. They used the branch at 1755749 for the create and first refresh, and at 452fa11 for the latest refresh. They do not exercise the create-once recovery code added in a22b0fe.

  • the owner's DM the owner's DM: publish created the page; refresh in place updated it.
  • a shared test room launch-video room a shared test room: publish created the page; refresh in place updated it.
  • Read-back: the ~218 KB page was written in pieces and read back whole: 218,451 bytes read against 218,270 rendered, with 11 <img> and all 10 h2 sections.
  • --name auto resolved "the agent's profile name".
  • Credentials loaded without sourcing the lane env.
  • --audience room rendered "Getting started with your agent", with 0 occurrences of the agent's name.
  • Copy works in the desktop app (confirmed by the owner).

Live receipts for the create-once path (head 16fafab, run by the coordinator):

  • Refused create, then retry. Scratch room a scratch room (the owner is not a member): pages.create was refused with FORBIDDEN ("the agent's owner may not publish in this room"). The marker kept state: creating and operation id the stored operation id. The rerun re-issued the same operation id and reported the refusal; no page was created.
  • Adopt by ownership marker. In the owner's DM the owner's DM, the marker entry was removed while the owned page (with its sutando-tutorial meta) still existed. publish adopted the page ("adopted": true, unchanged). That run also summoned the owner again, a duplicate card, which commit 6949462 fixes: adopting no longer summons unless --summon is passed.
  • Fresh create. In the same DM, after that page was removed from the list, plan returned create. publish then created the page through pages.create, with marker state complete. A rerun reported unchanged, with the same kind.
  • Read-back. 215,567 bytes read against 215,492 rendered; the sutando-tutorial meta sits at byte 140; the page shows "Picked for: Founder, Sales", 11 <img> and all the h2 sections.

Still unverified:

  • whether progress persists across a reload in the real HTML page runtime;
  • whether auto-advance works in the real runtime;

Worst-case disruption

The skill is opt-in: nothing runs it until the engine CLAUDE.md change lands. In the worst case the page fails to create and the owner sees only the greeting, which is today's behavior. The page is limited to one per DM, and each run is a constant number of room-commons calls. The shared channel_credentials extraction is behaviour-identical for pending-questions (same function body, now delegated), and its tests are unchanged and green. --name auto makes at most two 5-second HTTPS GETs before it falls back.

🤖 Generated with Claude Code

…d page

New `tutorial` skill. Voice mode is the existing walkthrough moved from
docs/tutorial-delivery.md. AG2 Space mode creates one "Getting started"
HTML page (Room Commons) in the owner's DM after the first-contact
greeting, with copyable fresh-install prompts and a pointer to
Settings -> Integrations, and summons the owner to it. A per-room marker
under <workspace>/state/tutorial/ makes re-link/reinstall refresh the
same page instead of adding another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonichi

sonichi commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner Author

Companion PR that wires this into first contact (engine CLAUDE.md lives in the desktop repo): the desktop first-contact change. Merge order: this PR first, then #842, then the engine bump.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 91%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • skills/pending-questions/scripts/pending_questions_room_db.py (100%)
  • skills/tutorial/scripts/getting_started.py (100%)
  • skills/tutorial/scripts/tour_page.py (100%)
  • src/channel_credentials.py (100%)

Summary

  • Total: 493 lines
  • Missing: 0 lines
  • Coverage: 100%

sonichi and others added 12 commits October 15, 2026 20:55
Owner-approved outline (Chi): eight parts, each one line on what it is
and one thing to try (Rooms, Commons, Teamwork, Calls & voice, Connect
other agents, Connect your apps, Activity & notices, Mobile), plus an
"Import your Claude Code history" part only on a Claude Code core.
`--core claude|codex|unknown|auto`; auto reads the engine's
resolve_core_runtime. The fresh-install list is folded into the parts.
UI labels checked against cinny-webclient main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tial fallbacks

The Getting started page (now tour_page.py) gets an inline-SVG hero and
one icon per section with AA-checked accents, a step-by-step tour
(Back/Next, step dots, Show all, arrow keys), per-section Mark done with
a progress bar kept in the page runtime's artifact.state (localStorage,
then memory, as guarded fallbacks), reduced-motion support, and no
external URL. Content is unchanged.

publish/render take --name auto (Matrix profile displayname, else the
mxid localpart, else Sutando). room_commons.py is found beside the skill
or in the installed skills dir, and its credentials come from the AG2
Space channel env file when the environment has none, through a new
shared src/channel_credentials.py that pending-questions now delegates
to instead of keeping its own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Mark done never writes the shared key: the per-viewer key is resolved
  before the checkboxes are enabled, and without a viewer id progress
  stays in this browser's localStorage (then memory).
- A thenable artifact.state.get is awaited.
- Show all: a step dot scrolls its section into view (instant under
  reduced motion) and moves focus to it.
- The Show all toggle keeps one label; aria-pressed carries the state.
- Copy fallback names the platform's keys; outcomes are announced in an
  aria-live status region instead of a static aria-label.
- No SVG pairs role="img" with aria-hidden.
- A head script sets .js so a scripted page opens on one step without
  painting all of them; without JS every section shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed publish

Copy (approved by Chi): a "Your DM" step first; Connect other agents
before Teamwork, with the client's descriptors (Dot by OpenAI, in
ChatGPT; Grok Bot in Grok; Muse by Meta); a try-it for Activity &
notices (Mention & Keywords); Calls & voice names only the headphone
icon in the room header and how an agent joins (it answers in your DM;
in a room you pick it under Bring an agent?); the import step starts
"If you use Claude Code" and says it reads the local history. Every
prompt says where to paste it, a closing card appears when all steps
are done, Mark done advances to the next open step, and the step title
replaces "Step N of M". Labels reconciled with current screenshots: +
at the top of the room list, room menu Invite, plug in the user card.

Screenshots: ten privacy-safe crops (assets/, 140 KB) adapted from
Zerlinda's Sutando's onboarding page, inlined as data: URIs. The page
is now ~218 KB, over one Room Commons write (196,608 bytes), so publish
appends in 150,000-byte pieces and rewrites a large change by clearing
in pieces; a small change is still one replace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The assets path walked two parents up, which the workspace-resolution
lint refuses; scripts/assets/ sits beside tour_page.py instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt cap

CI on Linux failed with E2BIG: one argv string may not exceed 131,072
bytes there, and pieces were 150,000. Pieces are now 120,000 bytes, and a
refresh is one replace only when both texts fit one argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Images: Rooms gets its own crop with the box on the + (it reused the
Activity bell); the Commons header crop gets the red box on its icon.
A test now refuses two steps sharing image bytes.

Page: progress and Mark done are hidden without JS instead of stuck
disabled; artifact.state.set is awaited inside try; an explicit
data-theme (on html, or copied from body) beats prefers-color-scheme;
the step change is announced once (heading focus, no aria-live on the
title); a repeat copy is re-announced; arrow keys act only inside the
tour and not on controls; Show all has a pressed style; a 3/10
position sits beside the step title, hidden from AT.

Copy: Commons names PDFs; Calls reads "start the call and pick your
agent under 'Bring an agent?'"; Activity names room menu → Notifications;
the apps alt is Settings → Apps → Integrations. --audience room says
"your agent" throughout, for rooms other than the owner's DM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"comes back here in your DM" and "Ask ... here" were wrong when the page
sits in a room; both now name the DM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it tracks them

The repo ignores assets/ except skills/*/assets/, so the new Rooms crop
under scripts/assets/ was never committed and CI could not find it. The
assets go back to skills/tutorial/assets/, and the one line that walks
up to the skill root carries the lint's line-scoped allow pragma.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ff-coverage gate

main() in-process for render, plan and publish (created, unchanged, exit 2,
3 and 4) against a stub room_commons.py, a temp workspace and channel dir,
and an env scrubbed of identity and credential variables; plus the
https helper (mocked urlopen), detect_core, _workspace, a run timeout, a
page-add without an html kind, rewriting an empty page, a marker whose
rooms is not a map, and channel_value's environment path. No network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ost answers

Review (Rui's Codex, P2): the page could be created twice when the marker
write failed, the process died, or a create's answer was lost.

- The page is made with room.commons.pages.create, which takes an
  operation_id: a retry with the same id returns the same page and
  writes its body at most once. The marker records `creating` with that
  id BEFORE the call, `pending` with the kind after it, and `complete`
  once every piece is written; any rerun resumes from the last entry.
- The create's first body carries an ownership marker
  (<meta name="sutando-tutorial" content="<agent mxid>">). With no marker
  entry, a listed "Getting started" page is reused only if its text
  carries this agent's marker; an empty or unrelated same-title page is
  never adopted or touched, and two marked pages or no agent id stop
  with exit 4 and the reason.
- No more page-remove cleanup or claims about it: a failed append leaves
  the page pending, and the next run finishes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and a roles line

Approved by Chi: no pre-written per-role prompt sets. When the agent knows
the owner's work (onboarding roles in the first-contact message, or
user_profile.md), it writes the prompts itself and passes them as
--prompts <json>, one per slot (dm, commons, teamwork, apps, import,
finale). Each is validated (string, one line, 1-200 chars, no link or
markup) and escaped on render; a bad entry, or a missing or unreadable
file, keeps that slot's generic prompt, and never fails the publish. The
slot, not the text, decides the DM/room paste hint. --roles adds a
"Picked for: Researcher, Founder" line under the heading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qingyun-wu
qingyun-wu previously approved these changes Oct 16, 2026

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at 70617b2 from a clean worktree of this head.

Ran: tests/tutorial-getting-started.test.py → 77 tests OK (1 loud skip: PageBehaviourTest when node+jsdom is absent); tests/pending-questions-room-db-serve-process.test.py → 15 OK, so the delegation to the new loader preserves behaviour; scripts/review-checks.sh, lint-sutando-home-path.sh, lint-workspace-resolution.sh, lint-skill.py skills/tutorial → clean.

Verified in code:

  • src/channel_credentials.py reads only the one lane env file resolve_channel_env selects, returns values to its caller and never prints them, short-circuits to {} when the environment already names any of the variables, and does not import or name any skills/ path. The 11 lines removed from pending_questions_room_db.py are logically identical to the loader. No new env var (AGENT_MXID already exists); manifest.json has no config block and the skill reads none.
  • Chunked publish: the default page is ~214 KB, so the chunk path is the main path. I checked its assumptions against the real client this seat has (room_commons_client.py:1775): replace is first-occurrence on the live text with UTF-8 byte offsets handled, and refuses when the text is absent; read --json returns the raw text; append on an html kind is verbatim. The test stub implements exactly those semantics, so the "final text equals the unchunked page" test stands on the real contract. One number to fix (nit 1).
  • --name auto: missing display name → localpart → "Sutando"; fetch failures swallowed; never raises. Display name is HTML-escaped (render --name '<b>x</b>&"y"' escapes every character); the 11 images are inline data URIs, no external URL in the output. --audience room substitutes "your agent" for the name and no DM data path exists.
  • Docs: tutorial-delivery.md keeps the voice procedure and states the page is additive; the src-map.md row matches the file header; catalog.json updated.

Nits (none blocking):

  1. SKILL.md:114 says pieces of "at most 150,000 bytes"; getting_started.py:60 has CHUNK_BYTES = 120_000. Keep the code's number: Linux caps a single argv string at 131,072 bytes, so the doc's figure would not even pass execve.
  2. "Never duplicated" rests on the marker file alone (state/tutorial/getting-started.json). A fresh workspace or a reinstall that drops state/ yields a second "Getting started" page; pages --json already returns titles, so a title fallback in plan() would close it.
  3. PageBehaviourTest skips wherever jsdom is absent; if CI does not install it, the eight JS-behaviour tests never run anywhere. A CI step or NODE_PATH would make them count.
  4. --audience room is a CLI option but SKILL.md says "run it only in the owner's own DM" — document the flag or drop it.
  5. test_the_token_is_never_printed guards only getting_started.py echoing its env (the stub never prints); narrow but not tautological.

Approving. The merge on sonichi/sutando is the owner's.

@qingyun-wu
qingyun-wu dismissed their stale review October 16, 2026 05:45

Dismissing my own approval: it was posted at f1ac2ba but the review read 70617b2; two commits (+500 lines: create-once lock/op-id, prompts/roles) landed in between. Re-reviewing at f1ac2ba now.

sonichi and others added 3 commits October 15, 2026 22:46
… audience, redacted errors

From Qingyun's review of 70617b2:
- SKILL.md stated 150,000-byte pieces; CHUNK_BYTES is 120,000 because
  Linux caps one argv string at 131,072 bytes. SKILL.md now says so, and
  a test fails if it names any other piece size.
- SKILL.md documents --audience room (only when the owner asks for the
  page in another room) instead of saying "DM only".
- A room_commons.py error is now redacted of every credential-like
  value in its environment (and each half of a url|secret token) before
  it becomes the publish error; the token test's stub now leaks its
  token, so the test checks the redaction rather than our own echo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…load as given

cinny-webclient#2231 ends the first-contact greeting with "I picked:
<Label1>, <Label2>." using persona labels (multi-word, e.g. "Product
Manager"), or slugs when labels are unavailable. --roles "Founder,
Sales." showed only "Founder": the slug-only pattern dropped "Sales.".

Entries are now split on commas, trimmed of whitespace and one trailing
period, and kept when they are 1-40 characters of letters, digits,
spaces and & - _ ' . (with at least one letter or digit). A slug-looking
entry is title-cased, a label is kept as written; duplicates are dropped
case-insensitively and at most six are shown, escaped. SKILL.md says to
pass the payload verbatim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xisting page

Live test at 16fafab: with the marker missing, publish adopted the owned
page (unchanged, adopted) and summoned the owner a second time, putting a
duplicate card in the DM. A summon now happens only when this page's
creation finishes (here, or resumed from our own pending entry) or with
--summon; adopting a page found by its ownership marker is not a
creation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review at 6949462 (delta 70617b2..HEAD: 5 commits, +642/−115), from a clean worktree.

Ran: tests/tutorial-getting-started.test.py 100 OK (one loud skip, jsdom absent on this host); tests/pending-questions-room-db-serve-process.test.py 15 OK; review-checks.sh PASS; lint-sutando-home-path.sh, lint-skill.py skills/tutorial, check-utc-z-strftime.py clean.

Verified in code

  • Create-once: a real fcntl.flock on state/tutorial/.getting-started.lock held across publish and released on exception; the creating entry with its op id is written (:404) before pages.create (:405), pending after, complete at :422, so a crash at any point resumes with the same op id and never creates twice, given server-side dedupe by operation_id. I checked that contract against the room-commons client this seat has at main c61464e: mcp_door(args) returns a connect callable (matches rc.mcp_door(Namespace(...))()), execute(room, action, arguments, operation_id=) is the signature used, and a non-completed outcome raises OperationPending ("may still take effect, so it is never re-run") — the same stance this PR takes. pages --json does carry by and copy_of.
  • Adoption: the <meta name="sutando-tutorial"> marker is read back via read --json text and compared exactly; zero tagged → create, one → adopt (writes the marker, next run unchanged), two or no agent id → exit 4; a same-titled untagged page is never touched.
  • --prompts: known slot, 1–200 chars, no CR/LF/</>, no :// or www.; bad or missing file → generic prompts; prompts are HTML-escaped in text and data-prompt (probed with <script> and &). --roles: comma split, [\w &'.-]{1,40}, Title-Cased, deduped, ≤6, escaped.
  • Chunks: test_skill_md_states_the_real_chunk_size ties SKILL.md to CHUNK_BYTES; ChunkTest asserts < 131_072; chunks() counts UTF-8 bytes per code point and never splits a character (probed with 4-byte emoji at limits 4/5/6).
  • No src/ change in the delta; no skill named from src/; no new env var; manifest.json has no config block.

Blocker — getting_started.py:309: create_page wraps the Actions-client exception unredacted. redact() (:200-209) is applied in run() and summon_error, but the PublishError(f"page create {operation_id} did not complete: {exc}") carries str(exc) straight into the {"ok": false, "reason": …} JSON on stdout (main() :518). Probe: an exception text 401 Authorization: Bearer abcdefghijkl at https://relay.x/r reached stdout verbatim. That defeats the "redacted errors" commit on the one call most likely to echo a URL or credential. Fix: redact(str(exc), self.env) there (and on the :312 path's kind!r for symmetry).

Nits

  1. tour_page.py:237-256 — role_labels("I picked: researcher, founder") → ['Founder']: the I picked: prefix is not stripped, so the first persona is lost when the agent passes the line verbatim, which SKILL.md:51-52 tells it to do. Strip the prefix and add the prefixed form to test_roles_take_the_first_contact_payload_as_given.
  2. getting_started.py:276-277 — resuming a creating entry never consults listed tagged pages; if the server's op-id memory has expired, a second page is created and the first orphaned. Checking for a tagged page before re-issuing the create closes it.
  3. getting_started.py:200-209 — redaction is value-based (env credentials ≥ 8 chars); a server-issued token in stderr is not masked. A Bearer \S+ pattern alongside would cover it.
  4. tour_page.py:222-235 — "first person" and "runs on a fresh install" are SKILL.md rules only; bare domains (example.com) pass BAD_PROMPT.
  5. Server-side dedupe of pages.create by operation_id is the one contract I could not exercise from this seat; a live create-retry round trip in the body would close it.

Requesting changes for the blocker; everything else is ready.

… review nits

Qingyun's re-review of 6949462 (REQUEST_CHANGES):
- Blocker: create_page wrapped the Actions client's exception into the
  publish reason unredacted, so a bearer or relay URL it echoed reached
  stdout. It now goes through redact(), as does the bad-kind message,
  and main() redacts every failure reason it prints. redact() also masks
  anything shaped like "Bearer <token>" or a url|secret pair, not only
  values found in the environment (nit 3).
- --roles strips a leading "I picked:" so the whole first-contact line
  can be passed without losing the first persona (nit 1).
- Resuming a `creating` entry first looks for a listed page this agent
  tagged and finishes it, instead of relying only on the server still
  remembering the operation id (nit 2).
- Prompts also refuse bare domains such as example.com (nit 4, the
  checkable half; "first person" stays a SKILL.md rule).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review at 1a1244a (one commit over 6949462), clean worktree.

Blocker closed. create_page now passes both the Actions-client exception and the bad-kind repr through redact(); main() redacts every printed failure reason as a backstop; and redact() gained a shape rule (SECRET_SHAPE) that masks anything that looks like Bearer … or a url|secret pair, not only values found in the env. Probed: 401 Authorization: Bearer abcdefghijkl at https://relay.x/r → 401 Authorization: Bearer [redacted] at [redacted]; a url|secret relay token → [redacted]; plain text untouched.

Nits taken: role_labels("I picked: researcher, founder") → ['Researcher', 'Founder'] (prefix stripped, trailing period too); a creating entry being resumed now reconciles against an already-listed page tagged by this agent before re-issuing the create, which closes the expired-op-id double-page case.

Ran: tests/tutorial-getting-started.test.py 103 OK (one loud jsdom skip), tests/pending-questions-room-db-serve-process.test.py OK, review-checks.sh PASS.

Approving. Merge on sonichi/sutando is the owner's; the one contract still best proven live is the server's create dedupe by operation_id, which the reconcile step now also covers if it ever lapses.

@sonichi
sonichi merged commit 8b564cc into main Oct 16, 2026
35 checks passed
@sonichi
sonichi deleted the feat/tutorial-skill-getting-started-page branch October 16, 2026 06:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants