Skip to content

feat(first-session): Run an agent here starts a hosted agent from Scout's welcome, in one click (TASK-290) - #2297

Merged
lilyshen0722 merged 5 commits into
mainfrom
lily/hosted-start-on-click
Oct 21, 2026
Merged

lilyshen0722 merged 5 commits into
mainfrom
lily/hosted-start-on-click

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Hold: this touches the hosted path, which is path 1 on Wednesday's call. Draft until the Wed 10-21 call ends; nothing presses before then.

TASK-290, ruling B. Walk 5 took 1:10.5 from the register page to a live agent, and ~10 s of that was the compact Run-it-here page between the welcome's button and the agent. On the person's own welcome, the button now does the work: one click installs and starts a hosted agent in their workspace. The click is the consent. The beta line (1 hosted agent per account, 30 turns a day) sits under the button.

The three conditions (Cofounder, TASK-290)

1. Runs only on the platform-posted welcome, decided by its author server-side.

  • services/welcomeHostedStart.ts: a message is "the welcome" only when its author is the Scout the reader installed in a pod they created. A field in the payload never decides it.
  • GET /api/messages/:podId (both the Postgres path and the Mongo fallback) drops the action from every other copy. A start that loses its action keeps its link to Bring your own, which works as before.
  • POST /api/hosted/welcome-start { messageId } repeats the same check before answering.
  • Only run-hosted runs in place. Scout's own hosted offer (commonly_offer_hosted_agent) and older welcomes carry no action, so they still link.
  • No client or agent route accepts a message payload; only server code writes one.

2. Only the workspace owner can run it, and a repeat click is idempotent.

  • The thread runs it only when the viewer created the pod. The server refuses everyone else with 403 not_welcome_start, and the thread then follows the link.
  • welcome-start answers either the hosted agent the person already holds or a free name (suggestLocalAgentName, now exported). Install and provision stay with their own routes, which remain the authority on cap, names and identity.
  • A failed install asks again. When a double click or a second tab got there first, the existing install or the cap now answers with that agent, and this click starts it: no second install, no error. An in-flight guard drops a second click that lands before the button disables.

3. Tests pin both.

  • welcomeHostedStart.test.js (real Mongo, Postgres lookup stubbed) covers:
    • the action kept on the owner's welcome;
    • the action dropped for other authors (another agent, another user's Scout, the owner, a member), for a reader who isn't the creator, and for a Scout installed by someone else;
    • refusals of non-owners, non-welcome authors, link-only starts, missing messages and malformed ids;
    • a free name when nothing is held;
    • the held agent on a repeat.
  • Mutation checks: dropping the author check fails 3 tests, dropping the creator check fails 1, dropping held-agent reuse fails 1.
  • messageController.test.js pins that both read paths serve through the gate, and hosted.test.js pins the route wiring and its 503.
  • V2ThreadStarts.test.tsx covers:
    • one click making exactly one welcome-start, one install and one provision call, with nothing sent;
    • a double click starting one agent;
    • another tab winning via the existing install, and via the cap;
    • a non-owner following the link;
    • a server refusal following the link;
    • a refused name opening the page;
    • a failure showing a line under the button.
  • useWelcomeHostedStart.test.tsx pins the in-flight guard; removing it fails the test.

Also in here

  • onboardingMarks.markWelcomeHostedStart: this path never opens Bring your own, so byoOpenedAt won't be set for people who start here. The click records firstStartId: 'run-hosted' under the same write-once rule instead, so the funnel still shows which start they picked.
  • DEFAULT_INSTALL_SCOPES moved to v2/utils/agentInstallScopes.ts so the welcome and Bring your own install with the same scopes.
  • No new strings. The button, beta line and errors reuse existing keys, so zh-CN is already covered.
  • Renaming the agent moves to the agent card in a second PR (Cofounder: ship B without it). Until then the agent keeps <username>-agent.

Browser check

Built from this branch and served on 127.0.0.1, with every /api call mocked.

  • 1440: the beta line sits 4 px under the button and is its accessible description. "Starting…" is disabled while the calls run. Afterwards the start is gone and the other two stay. The network shows one welcome-start, one install, one provision, and no navigation.
  • 390: the button is 44 px tall, the line sits under it, and the page has no sideways scroll (scrollWidth 390). The error line reads "Hosting isn't available on this instance yet…" and the button is enabled again.

Screenshots are in the pod: 1440 before, 1440 starting, 1440 live, 390 before, 390 error.

Done means

Walk 6 at a human pace shows under 60 s, from the register page appearing to the agent being live. That's measured after deploy. If B alone misses, (A) drops the username field from register.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

SECURITY-GATE: PASS @ 9aea8fe

Independent security read of the TASK-290 one-click hosted start. This is a Cofounder operator session; not a Commonly seat.

What I checked at this head:

  • Who may run it. workspaceScoutUserId requires pod.createdBy === caller and an active Scout installation the caller made in that pod, and the message's user_id must be that Scout's bot user. Both the read gate (gateWelcomeHostedActions, PG and Mongo paths) and POST /api/hosted/welcome-start decide this from the stored author. The payload action field is never trusted. On the Mongo path, normalizeMongo takes _id from the populated user, so the author compare is a plain id compare, and an unmatched author drops the action (fails closed).
  • What the endpoint can do. welcome-start only answers the caller's own held hosted agent or a free name from suggestLocalAgentName(…, caller). It writes nothing except the write-once onboarding mark. Install and provision still go through /api/registry/install and /api/hosted/provision with their own auth and cap, so the new route adds no install authority.
  • Input. messageId must match ^\d{1,15}$ before the PG lookup. The route sits behind hostedRateLimit and auth, and requireConfigured runs first.
  • Repeat clicks. The client blocks a second click while one is in flight. A failed install re-resolves, so a second tab's click resolves to the agent the first click installed, and the account cap stays the backstop.

Non-blocking note: two simultaneous first clicks from two tabs can both reach /api/hosted/provision for the same agent. That is not a security issue. Walk 6 can confirm the agent stays live after a double-tab click.

Holds pending green CI at this head; I'll re-read if the head moves.

GATE: PASS

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Security gate — #2297 at 9aea8fe5

Measured at 9aea8fe538ee5097f149243be5a4048bb3d9a039 in a detached worktree: draft, base main, merge-base 829b65bc = origin/main exactly (base unmoved, so nothing in the queue can author a state neither of us ran). 1 commit, 21 files, +886/−44. mergeStateStatus: CLEAN. CI at this SHA: 17 check-runs, 14 success, 3 skipped, none failed, none cancelled.

Baseline. Backend: the five touched suites 98/98; 43 related suites (approvalActionService, hostedRuntime, routes/registry) 255/255; tsc --noEmit -p tsconfig.typescheck.json 0 errors; eslint on the six changed .ts files 0 errors (20 pre-existing max-len warnings). Frontend: the two named suites 27/27; all 8 V2Thread* suites 113/113; v2-layout-invariants 213/213 (v2.css adds two new classes and overrides nothing).

1. Who decides which message is the welcome — confirmed on three independent legs

The predicate. workspaceScoutUserId requires pod.createdBy === userId, an active AgentInstallation for agentName: 'scout' in that pod with installedBy: userId, and a bot User whose username is buildAgentUsername('scout', instanceId) with isBot: true. Both the read gate and the route then require message.user_id === that bot id. The payload's action is necessary to ask; it never authorises.

The writers. I enumerated every producer of a kind: 'starts' payload rather than trusting the docstring — there are three, and only one sets the action:

writer start action
authController.ts:320 (signup welcome) run-hosted run-hosted
nativeRuntimeService.ts:700 (commonly_offer_hosted_agent) run-hosted + href none → isHostedAction false, keeps its link
approvalActionService.ts:1051 (new-pod next steps) connect-agent-here, invite-teammate invite

No untrusted writer can create the field at all. The human create path destructures { content, text, attachments, replyToMessageId, threadRootId } (messageController.ts:176) and the agent runtime post route destructures { content, metadata, messageType, replyToMessageId, threadRootId } (agentsRuntime.ts:2075) — neither accepts a payload, and agentMessageService states it is server-composed. So the author check is defence behind a surface a caller cannot reach, not the only thing standing in the way.

2. Both read paths — confirmed, and each independently covered

getMessages gates the Postgres result (:148) and the Mongo fallback (:161). The two are shaped the same: normalizeMongo emits user_id as a string (pulling _id out of the populated user), which is the field the gate compares, so the Mongo copy is not silently ungated by a field-name mismatch. Removing the gate from either call site alone reddens a test (M4, M5 below). A failed lookup inside the gate resolves to null and every action is dropped — fail-closed — and a throw cannot double-send, since res.json is only reached with an already-awaited value.

3. The route — confirmed

POST /api/hosted/welcome-start runs hostedRateLimit, auth, re-checks the full predicate through resolveWelcomeHostedStart, and answers only { podId, existing, agentName } where existing comes from listHostedInstallationsForUser(userId) — the caller's own. It installs nothing: backend/routes/registry/ has no diff in this PR, and hosted.ts's only removed line is an import edit, so /provision is untouched too. messageId is checked against /^\d{1,15}$/ before PGMessage.findById.

"A repeat click never makes a second install" is enforced by two server mechanisms, neither of them new here. Worth naming, because the client's inFlight ref only covers one tab:

  • the hosted cap — DEFAULT_AGENTS_PER_USER = 1, applied by hostedCapRefusal in routes/registry/install.ts, so once the first install is written a second returns hosted_cap_reached;
  • that cap is read-then-write, so two genuinely simultaneous clicks can both see used: 0. They are backstopped by uniqueness: suggestLocalAgentName is deterministic, so both clicks ask for the same name, and the loser hits Agent already installed in this pod (install.ts:375) or agent_name_taken / agent_username_conflict (:260, :330, :401). Either way the second install writes nothing.

4. The hook — confirmed

On install failure it re-resolves and, if the plan now carries existing, provisions that agent; a 409 becomes NameRefused and opens the linked page instead. DEFAULT_INSTALL_SCOPES is the BYO page's old DEFAULT_SCOPES extracted verbatim — same eight scopes, same order, and V2AgentBYO.tsx now imports it, so the welcome path cannot ask for more than the page does.

Mutations

Each applied alone; worktree git status clean after each.

mutation result
drop the pod.createdBy check 1 red
route stops comparing the message author 2 red
read gate stops comparing the message author 1 red
gate removed from the Postgres path only 1 red
gate removed from the Mongo fallback only 1 red
hook: no re-ask after a failed install 3 red
hook: same-tab in-flight guard removed 1 red
drop installedBy from the Scout lookup survives — see below

The survivor, and why it is a coverage gap rather than a hole. Removing installedBy: uid from the AgentInstallation lookup changes no test, and property 1 names that conjunct explicitly. It is not exploitable: the pod-creator check still holds, and the route's answer is caller-scoped — existing is the caller's own agent and any install runs under the caller's own identity and cap — so the worst a dropped installedBy buys is the button appearing on a welcome authored by a different Scout instance in the creator's own pod, after which the creator installs their own agent. Still worth one arm: a welcome from a Scout the pod creator did not install should be refused, or the tightening is free to regress.

Note (not blocking)

The gate strips action but not caps, and the note renders without the action. gateWelcomeHostedActions removes exactly one key — const { action: _action, ...rest } = start — while V2ThreadMessages.tsx renders the limits line on {start.caps && …}, independent of whether the action survived. caps comes from hostedCapsForOwner(userId) → hostedCaps({ pro: await ownerIsPro(userId) }), i.e. turnsPerDay 30 or 200, so the stored welcome payload records the workspace owner's plan tier at signup and shows it to anyone later added to that pod — including the readers for whom the action was deliberately dropped. One bit, not credentials and no privilege change, and at signup there is nobody else in the pod yet; but it is a cross-reader signal on the path this PR adds, and the fix is a line: drop caps in the same map, or render the note only where the action survived.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: PASS

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Security re-gate — #2297 at 86a2fae0

The prior clearance was for 9aea8fe5; a code-touching push spends it. Measured at 86a2fae07d70ec79c6932f8c0467db4d620b1c3f: still draft, base main, merge-base 829b65bc = origin/main (base unmoved). One new commit, 2 files, +7/−5.

The delta is bounded by measurement, not by the commit message. Blob-compared all 21 files of the PR between the two heads: 19 identical, only backend/services/welcomeHostedStart.ts and its suite changed, and the file sets are equal. So every finding from the 9aea8fe5 verdict carries unchanged — the whole frontend, the route, the controller, authController, onboardingMarks, the scopes extraction.

The fix is what was asked for, one destructure: const { action: _action, caps: _caps, ...rest } = start;. href, label and id survive, so the start keeps its link.

Measured

  • welcomeHostedStart suite 13/13; the five affected backend suites 98/98; tsc --noEmit -p tsconfig.typescheck.json 0 errors; eslint on the changed service clean.
  • Mutations, each alone, worktree clean after each:
mutation result
revert the caps strip (keep only action) 2 red
strip caps but keep action 3 red

So both halves of the redaction are independently pinned, in both directions.

One thing worth saying about the old pin: at 9aea8fe5 the suite asserted the leak — expect(start.caps).toEqual({ agentsPerUser: 1, turnsPerDay: 30 }) on a non-owner's copy. The suite was green on the defect because the defect was written down as the expectation. This commit inverts that line and adds the owner-retention assertion beside it.

The residual: a sibling route still serves the payload ungated

The gate covers messageController.getMessages — both the Postgres path and the Mongo fallback — and that is the route the v2 shell reads (useV2PodDetail → /api/messages/:id), so the feature's own UI is fully covered. But it is not the only HTTP reader of those rows. I enumerated every caller of PGMessage.findByPodId:

reader gated? serves payload?
messageController.getMessages (PG + Mongo) yes yes
routes/showcase.ts:217 no no — a whitelist serializer emits {id, content, messageType, createdAt, userId} only
controllers/pgMessageController.getMessages no yes — res.json(messages) on the raw rows

pgMessageController.getMessages is mounted at GET /api/pg/messages/:podId (server.ts:421) behind auth + isPodMemberInMongo, and the v1 ChatRoom.tsx (:941, :1110) still calls it. So a pod member can obtain the owner's caps — and the action — from that route, which is precisely the audience the redaction exists for, and the claim "dropped from every copy that fails (1)" is not true of it.

Why this is a note and not a hold: v1 never reads a message payload (its only payload references are socket handlers for tasks, presence and typing), so nothing renders it; the disclosure is one bit — free vs Pro at signup — to someone already in the pod; and the action grants nothing, because POST /api/hosted/welcome-start re-checks the full predicate and answers only with the caller's own agent. The fix is the same one line the sibling controller already carries: wrap that res.json in gateWelcomeHostedActions.

Still open from the previous verdict, now with the mechanism identified

installedBy is unexercised, and the reason it looks covered is a test name that claims it. The arm drops it when the pod's Scout was installed by someone other than its creator arranges createTestPod(Pod, member._id, …) and then installScout(theirPod._id, owner._id, 'uada') — a pod the reader did not create, with Scout installed by the reader. Two mutations separate the conjuncts:

  • drop installedBy: uid from the AgentInstallation lookup → 13/13 green;
  • drop the pod.createdBy check → that arm is the only one that reddens.

So the arm tests createdBy under a name asserting installedBy, which is why reading the test list suggests coverage that is not there. Still not exploitable, for the reason given last time (the answer is caller-scoped and any install runs under the caller's own cap and identity) — but an arm that arranges a Scout installed by a third party in the reader's own pod would close it, and the current name should move to that arm.

CI

11 success, 2 skipped, and Test & Coverage was still in_progress when I measured (E2E Tests went green during the gate). Nothing failed or cancelled. My own run of the five suites the delta can reach is green, which is the part of that check this change touches; the rest of it is unmeasured by me at this head.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: PASS

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Security re-gate — #2297 at 2df2a239

The 86a2fae0 clearance is spent. Measured at 2df2a239db5baca1ec50b979caca3dbcde7d633e: still draft, base main, merge-base 829b65bc = origin/main (base unmoved). One new commit.

Delta bounded by blob compare across the PR's whole file set: exactly 3 files differ from 86a2fae0 — controllers/pgMessageController.ts, its suite, and the rewritten installedBy arm. services/welcomeHostedStart.ts is byte-identical, as is every frontend file, so the rest of the two prior clearances carries.

Both items I raised are addressed. Six affected suites 108/108; tsc --noEmit -p tsconfig.typescheck.json 0 errors; eslint on both changed controllers clean.

The legacy route — closed

pgMessageController.getMessages now returns await gateWelcomeHostedActions(messages, podId, userId), placed after the 404/401 contract and after isPodMemberInMongo, so the careful ordering comment above it is undisturbed. Two mutations, each alone:

mutation result
remove the new gate call 1 red
pass a different caller identity to the gate 1 red

The new arm mocks the service and asserts the exact call triple (raw, 'p1', 'u1') plus that the gated rows are what res.json receives. That is a wiring test by design — the behaviour is covered in the service's own real-Mongo suite — and because it pins the argument triple, a wrong identity or a swapped argument order cannot pass. The caps/action strips still bite at this head: reverting either half reddens 3 arms.

The reader census is now closed for HTTP paths. Every caller of PGMessage.findByPodId: messageController.getMessages (PG + Mongo fallback) gated, pgMessageController.getMessages gated as of this commit, routes/showcase.ts safe by whitelist serializer (never emits payload). The two remaining Mongo-side readers — nativeRuntimeService.ts:1208 and agentMessageService.ts:2252 — build an agent's turn context rather than an HTTP response to another user, so they are not in this disclosure class.

The installedBy arm — fixed, and the coverage traded places

The rewrite is right: pod created by the owner, Scout installed by someone else, message authored by that other Scout. installedBy is now genuinely covered — dropping installedBy: uid from the lookup reddens that arm, where it survived at both previous heads.

But the same mutation pair, re-run, shows the gap moved rather than closed:

mutation at 86a2fae0 at 2df2a239
drop installedBy: uid survives 1 red
drop the pod.createdBy check 1 red survives

I ran the createdBy mutation across all six affected suites, not just the two in the delta: 108/108 green. The reason is that the other candidate arm — "drops it for a reader who did not create the pod" — is now satisfied by the installedBy leg instead: that reader did not install the pod's Scout either, so the lookup misses and the action drops without createdBy ever being consulted.

Nothing in the shipped code got weaker; both checks are live, and the conjunction is still correct. What changed is which leg a regression would be caught on. Same severity as before (a coverage gap, not a hole — the route's answer stays caller-scoped and an install runs under the caller's own cap and identity). The fix is two arms rather than one: hold the installer constant and vary the creator for one, hold the creator constant and vary the installer for the other. The second now exists; the first is what the older arm used to be.

CI

12 success, 2 skipped, Test & Coverage still in_progress when I measured — same as at the previous head. Nothing failed or cancelled. My own run of the six suites the delta can reach is green; the rest of that check is unmeasured by me at this head.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: PASS

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Security re-gate — #2297 at ad370b73 (test-only)

Measured at ad370b735f24e5c2da196fa37a2411534491cede: base main, merge-base 829b65bc = origin/main (base unmoved).

Test-only confirmed by blob compare, not by the commit message. Across all 23 files of the PR, 2df2a239 → ad370b73 leaves 22 identical — including every one of the 14 non-test files — and the only change is backend/__tests__/unit/services/welcomeHostedStart.test.js (+13/−1). So the 2df2a239 clearance of the shipped code carries in full, and what is left to check is whether the new arm does what it claims.

It does. The conjunction now has one arm per leg, each failing alone and for its own reason:

mutation the single arm that reddens
drop only the pod.createdBy condition drops it when the reader installed Scout but did not create the pod — new
drop only installedBy: uid drops it when the pod's Scout was installed by someone other than its creator

Both across the same six suites, 109/109 at baseline. The new arm is the complement of the other: the pod is created by someone else while the reader is the installer, so installedBy is satisfied and only createdBy can drop the action — and it fails for that reason rather than a fixture error, Received: "run-hosted" against toBeUndefined(). The older arm, which is satisfied by whichever leg fails first, has been honestly renamed to drops it for another reader, even on the real welcome.

That closes the coverage item I carried through three heads: at 86a2fae0 createdBy was pinned and installedBy was not; at 2df2a239 they traded places; here both are pinned independently.

CI at this SHA, as measured: 8 success, 4 in_progress (Test & Coverage, E2E Tests, Chart Lint, Analyze (javascript-typescript)), CodeQL neutral pending that analysis. Nothing failed or cancelled. For a delta of one test file my own runs cover its whole blast radius, which is why this does not hold the verdict — but the heavy checks have not reported at this head.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: PASS

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

SECURITY-GATE: PASS @ ad370b7

Delta re-read from 9aea8fe, the head of my earlier PASS. This is a Cofounder operator session; not a Commonly seat.

  • 86a2fae: the read gate now drops caps along with action on every copy that isn't the viewer's own welcome, so other readers no longer see the owner's plan limits. It's a one-line destructure, and the gate logic is otherwise unchanged.
  • 2df2a23: the legacy GET /api/pg/messages/:podId now serves through the same gateWelcomeHostedActions, which closes the second read path that returned raw payloads. The gate is applied after the existing access checks in that controller, and nothing else in the handler changed.
  • ad370b7: test only. There is one arm per leg of the creator-and-installer check.
  • The frontend, the welcome-start route and the install/provision path are unchanged from 9aea8fe. Every commit in the delta is authored by Lily Shen.

Non-blocking: the stored welcome payload still carries caps. Agent context reads of the owner's own workspace serve it raw, but only agents the owner installed are in that pod, so I'm not asking for a change here.

GATE: PASS

lilyshen0722 and others added 4 commits October 19, 2026 21:43
…ut's welcome, in one click (TASK-290)

Walk 5 spent ~10 s on the compact Run-it-here page between the welcome's
button and a live agent. On the person's own welcome the button now does the
work: one click installs and starts a hosted agent in their workspace, with
the beta limits under it. The click is the consent.

Which message may run it is decided server-side by its author: the Scout the
reader installed in a pod they created (services/welcomeHostedStart.ts).
GET messages drops the action from every other copy, and the new
POST /api/hosted/welcome-start checks the same thing before answering. It
answers the hosted agent the person already holds, or a free name; install and
provision stay with their own routes. A failed install asks again, so a double
click or a second tab gets the same agent back instead of a second install or
an error. A refused name opens the page, where a name can be chosen; every
other copy keeps the link to that page.

The pick is recorded as the person's first start (firstStartId 'run-hosted'),
since this path never opens Bring your own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG
Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
Agent-Model: codex/gpt-6-sol/medium
Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
Agent-Model: codex/gpt-6-sol/medium
Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
Agent-Model: codex/gpt-6-sol/medium
@samxu01
samxu01 force-pushed the lily/hosted-start-on-click branch from ad370b7 to fa6dbbb Compare October 20, 2026 04:44

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-gate — #2297 at fa6dbbb98818524fe7989d90095bbce5a2348acd

The rebase carries, measured independently

bec5d6c7 is an ancestor of this head, the branch is 4 commits, and my own git range-diff against the head I passed (ad370b73, base 829b65bc) reports all four as = — patch-identical. File sets are identical between the two PR diffs, 23 files each.

Patch identity is not content identity once the base moves, so I compared every one of the 23 files blob-for-blob between ad370b73 and this head. 20 identical; 3 moved — and the ask names one of them:

file moved by
backend/config/native-agents/scout.ts main's prompt additions (#2347 and the lines beside it)
frontend/src/v2/hooks/useV2PodDetail.ts not named in the ask — main's reconnect catch-up work
frontend/src/v2/v2.css not named in the ask — main's .v2-pods__search-kbd rules

Neither unnamed file interacts. In useV2PodDetail.ts this PR adds one optional caps? field to the V2Start interface while main's five hunks are the reconnect merge/dedupe path — no shared line or identifier. In v2.css this PR adds .v2-chat__start-note and --error; main added .v2-pods__search-kbd rules. One definition each, no selector collision, and --v2-danger is a defined token (v2.css:92), so the error note is not an undefined-var colour.

On the contradiction the ask asks about: there is none. This PR's only change to scout.ts is the JSDoc above SCOUT_HOSTED_START; every line main gained is inside the prompt string from :86 down (model naming, the OpenCode route, DeepSeek, which models share a pod). Different regions, nothing restated.

Suites at this head: the 6 backend suites this PR touches plus nativeAgents.scoutKnowledge and docsSiteFacts → 131 passed / 8 suites, matching the ask. Frontend V2ThreadStarts + useWelcomeHostedStart + the two suites covering the moved files (useV2PodDetailReconnect, v2-layout-invariants) → 244 passed / 4 suites.

Defect — the shipped prompt tells Scout the button does what this PR stops it doing

scout.ts:68-70, prompt text the model reads:

- Your welcome offered starts as buttons. Where Commonly can host agents it led with "${SCOUT_HOSTED_START.label}", which opens Bring your own with "Run it here" chosen and sends you nothing.

After this PR a click on that start, on the person's own welcome, starts a hosted agent in place: V2ThreadMessages routes run-hosted into useWelcomeHostedStart, which POSTs /api/hosted/welcome-start and installs and provisions without navigating. The Bring-your-own page is only the refusal fallback — onOpenPage fires for not_welcome_start, agent_name_unavailable, or a refused name. So the sentence is false for exactly the path this PR adds, and true only for a reader who is not the owner. It is stated unconditionally, and the welcome's primary reader in a first session is the owner.

The PR's author updated the comment and not the prompt: the JSDoc 30 lines above now reads "on the person's own welcome a click starts a hosted agent in place … anywhere else its href … opens Bring your own". The prompt below it was not touched.

Provenance, and my own miss. That line is not a rebase artifact. It is byte-identical across the PR's old base 829b65bc, the head I passed (ad370b73), main today, and this head; git log -S attributes it to #2259. So it was present and already contradicted by the behaviour at the head I passed on 10-17, and I did not catch it. The rebase did not introduce it — this PR does, because before this PR the sentence was accurate. Naming it now rather than carrying my earlier PASS forward.

Nothing pins the sentence: no test asserts it (scoutKnowledge asserts the hosted-offer tool and the model line, not the welcome start's destination), so the fix is one sentence in the prompt and no test churn.

Verdict

The rebase itself is clean and the content is the content I passed. Held on the prompt line above: a first-session agent that misdescribes the button the person just pressed is the defect this PR's own purpose is most exposed to, and the correction is a single string.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: HOLD

…ays so

Vera on #2297: Scout's prompt still said the welcome's "Run an agent here"
"opens Bring your own with Run it here chosen". With this PR, a click on
the person's own welcome starts a hosted agent in place
(services/welcomeHostedStart.ts); the page is only the fallback for a
start that cannot run. The JSDoc was updated and the prompt line was not.
The line now says both, and scoutKnowledge pins it (it fails on the old
line).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-gate — #2297 at 8b01affb22a7b80646a65423876ebcf381abf9e1

My HOLD at fa6dbbb9 is an ancestor; one commit since, two files, +13 −2. Blob-compared all 24 files in the PR against the held head: the only two that moved are backend/config/native-agents/scout.ts and backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js. (The file set is 24 rather than 23 because the scoutKnowledge test joins the PR's own diff with this commit.)

The defect is fixed, and the sentence is now true of the code

The prompt reads: the start led with "<label>". On the person's own welcome, a click on it starts a hosted agent right here in their workspace, with no page in between; only if that cannot run does it open Bring your own with "Run it here" chosen. Either way it sends you nothing.

That matches what I measured when I held: V2ThreadMessages routes run-hosted into useWelcomeHostedStart, which installs and provisions in place, and onOpenPage fires only for not_welcome_start, agent_name_unavailable or a refused name — a fallback, not the path. The kept clause is also still true: the welcome's start posts no message as the person (services/onboardingMarks.ts:19 — "sends no message: it records firstStartId"), which is what distinguishes it from the two starts that do send the label as the person's own message.

The new arm is load-bearing in all three of its assertions

Measured separately, worktree restored and tree identity (16595292…) re-verified between runs:

# mutation result
M1 restore the old stale sentence verbatim fails — /click on it starts a hosted agent right here in their workspace/
M2 keep the in-place claim, drop only the fallback clause fails — /only if that cannot run does it open Bring your own/
M3 keep the new text and re-add the stale sentence beside it fails — the negative not /nothing to install", which opens Bring your own/

M1 is the one the ask measured. M2 and M3 are the ones that matter for what happens next: M2 means a future edit cannot leave an unconditional "runs in place" claim that would be wrong for the non-owner reader, and M3 means the negative assertion is not dead weight — a leftover copy of the old sentence cannot survive beside the new one. Backend suites at this head: scoutKnowledge + welcomeHostedStart = 31 passed / 2 suites, matching the ask.

Finishing the census my HOLD only implied

I held saying the fix was one sentence; that was a claim about the whole repo and I had only grepped scout.ts. Done properly now, at this head, for every copy of prose describing that button — and every other one already describes the in-place behaviour: backend/controllers/authController.ts:305 ("One click starts a hosted agent right here"), backend/services/onboardingMarks.ts:19, the guideIdentity test's comment block, frontend/public/llms.txt:6, frontend/src/v2/components/V2Thread.tsx:356, frontend/src/v2/hooks/useWelcomeHostedStart.ts:35. No stale copy survives anywhere, so the prompt line was the entire exposure.

New since the HOLD: main moved again, into two files this PR touches

origin/main is now cd26f2a3, which is not an ancestor of this head — #2338 and #2340 landed, and both touch frontend/src/v2/components/V2Thread.tsx and frontend/src/v2/v2.css, which this PR also edits. The press merges onto that, so I measured the merged state rather than only the head:

  • git merge-tree --write-tree origin/main <head> → clean, tree 1b10ef8d.
  • Hunks are far apart: in V2Thread.tsx the PR edits around lines 26 / 348 / 475 / 1636 and main's compact-header work sits at 1564 / 1591; in v2.css the PR's block is at 1998 and main's at 1051.
  • On the merged tree I ran both sides' frontend suites — the PR's V2ThreadStarts, useWelcomeHostedStart, useV2PodDetailReconnect, and the suites main's two commits touch (v2-layout-invariants, v2-type-floors, V2ThreadStarter, V2Composer, V2PodsSidebar.scale, i18n): 326 passed / 9 suites.
  • The 15 backend files are blob-identical between this head and the merged tree, so the 31-test backend run above carries to the merged state unchanged.

Verdict

The held defect is closed by a prompt line that matches the code, pinned by an arm whose every assertion I reddened on its own; the rest of the PR is the content I passed on 10-17; and the merged state against the main that moved under it is clean and green on both sides' suites. No defect.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: PASS

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.

1 participant