Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,26 @@ one path, deltas to pixels:
synthetic in-progress message alongside the persisted timeline, until
the real message lands and replaces it.

## Workbench Definition and hostless onboarding

A picker "template" is a shipped **Workbench Definition**, not a second
kind of object: default agents, routines, tools, required and optional
plugins, and an ordered onboarding walkthrough. Creating from a named
row mints an empty workbench channel with no host, then instantiates
that definition into the room. The walkthrough is posted as a system
timeline card, never as a side effect of hosting an agent — so an
empty channel can onboard with nobody launched.

Code review's definition names three reviewers and does not name Myra.
GitHub already connected is the same in-room card: it reads live
credential state and flips to repository pick. There is no separate
create-dialog path for the already-connected case.

Settling a connector a template room is waiting on records the
connected event from a system address and does not wake an agent. A
generic in-room connect that an agent asked for still wakes that
agent.

## Capability growth and approval gates

An agent's capability set grows through what it is granted, not through
Expand Down Expand Up @@ -145,6 +165,8 @@ does not maintain a parallel scheduler.
- [docs/workbench-tenancy.md](docs/workbench-tenancy.md) — workbench tenant
mint, listing, and move mechanics
- [docs/needs-you.md](docs/needs-you.md) — the approval surfacing model
- [docs/connect-cards.md](docs/connect-cards.md) — in-room connect cards
and template-room settle
- [VENDORED.md](VENDORED.md) — the vendoring ledger for `@intx/*`

## Open questions
Expand Down
33 changes: 29 additions & 4 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,41 @@ Deployment is explicit via **Pulumi**, targeting **Railway**. CI runs
tests only — nothing auto-deploys on `main`; a deploy is a deliberate,
separate action.

## Workbench Definition (shipped)

`WorkbenchDefinition` (`WorkbenchDefinitionSchema` in
`@corbits/workflow-catalog`) is the one type for a named picker row:
default agents, routines, tools, plugins `{required, optional}`, and
ordered `onboardingSteps`. A template is a shipped definition, not a
second kind. `instantiateWorkbenchTemplate` resolves the definition
against a bench over injected ports, including `beginOnboarding(steps)`.

Create (`apps/web`'s `instant-agent-create.ts`) mints an empty
`kind: "workbench"` channel with no host and no `definitionId`,
instantiates, then `POST /workbenches/:id/onboarding`
(`packages/chat/src/routes.ts`) posts the walkthrough from
`system@<workbenchId>` — a `connect-github` block carrying the
definition's title, promise, and step labels. The card is not posted as
a side effect of hosting an agent.

`settleConnectedService` (`packages/chat/src/connect-pending.ts`)
clears both `connections/pending` and `template/pendingConnections`. A
template-key-only match posts `connection.connected` from the system
address and does not `dispatchTurn`. A generic pending match still
wakes the asking agent.

## GitHub connect (shipped)

The in-room `connect-github` card and the Plugins/Connections GitHub row
are **PAT-first** (CL-6345): the person pastes a personal access token;
the host tests and stores it through `@workbench/connections`' generic
`github/complete` route. The card then flips in place to pick repos
(`startReviewingRepos`). A GitHub App / hosted OAuth welcome mat is
CL-6343 and is not the current product path — do not document OAuth as
the first Connect step, and do not treat a PAT paste as a defect against
an OAuth-first welcome mat that has not shipped.
(`startReviewingRepos`), including when GitHub is already connected —
the in-room card reads live state; there is no `/new` already-connected
dialog. A GitHub App / hosted OAuth welcome mat is CL-6343 and is not
the current product path — do not document OAuth as the first Connect
step, and do not treat a PAT paste as a defect against an OAuth-first
welcome mat that has not shipped.

Optional `GITHUB_APP_CLIENT_ID` / `GITHUB_APP_CLIENT_SECRET` exist for
that future hosted path; leaving them unset is normal. See
Expand Down
45 changes: 23 additions & 22 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ Create stays on `/new` (`apps/web/src/pages/new-workbench-picker.tsx`):
a prompt box is the primary act: typing a goal and submitting mints an
empty channel and sends that text as the first message; blank plus
invites nobody. Named-template rows underneath mint that same empty
channel, then invite existing principals (including Myra as a
participant, never as mint `definitionId`) — one-click shortcuts, not
a kind-then-Create second step. There is no Describe door and no
channel with no host and no mint `definitionId`, then instantiate the
picked Workbench Definition's own agents (Myra joins only when the
definition names her) — one-click shortcuts, not a kind-then-Create
second step. There is no Describe door and no
`describe-first-workbench.tsx`.

A bench that already has one or more workbenches skips first-run and
Expand All @@ -88,19 +89,19 @@ put there.

### Code review's first minute

Code review is the product scene for the template path: Connect GitHub
with a personal access token (the shipped path today) → the connect card
flips in place to pick repositories → reviewers introduce themselves as
left-aligned messages with avatars. A GitHub App / hosted OAuth welcome
mat is future work (CL-6343), not current product.
Code review is the product scene for the definition-driven path:
minting the Code review workbench opens an empty room with no host —
its Workbench Definition names three reviewers and no Myra. The room
itself posts the onboarding card: Connect GitHub with a personal access
token (the shipped path today). The same in-room card reads live
connection state and flips in place to pick repositories — already
connected GitHub is that card, not a `/new` dialog. A GitHub App /
hosted OAuth welcome mat is future work (CL-6343), not current product.

Connected/settle honesty — no stale Connect after success; settle never
posting as the signed-in user; no agent 401 after GitHub already
succeeded — is the **target**, not shipped end-to-end. What ships today
is narrower: live credential reads that resolve at call, and a GitHub
settle path that can still attribute as the connecting user. Point
implementers at IMPLEMENTATION.md open questions, CL-6737, and CL-6738;
do not treat those three guarantees as current product law.
Settle for a template-key-only wait posts from a system sender and
does not wake an agent. Generic `connections/pending` still wakes the
asking agent. Neither path posts the connected notice as the connecting
person.

## Plugins and Skills

Expand Down Expand Up @@ -171,8 +172,9 @@ User-facing surfaces (UI, docs, support) use exactly these nouns:
- **DM** — the one 1:1 conversation with an agent. Never cloned by a
second open.
- **Channel** — a shared room between people and agents. Plus mints an
empty one; nobody is auto-hosted. Named templates invite existing
principals into that room.
empty one; nobody is auto-hosted. Named templates instantiate their
Workbench Definition's own agents into that room (Myra joins only
when the definition names her).
- **Bench** — the shared team scope a person signs into and switches
between; shown in the bench switcher, never called a "workspace" or
"org" in copy.
Expand All @@ -186,11 +188,10 @@ user-facing surfaces use the rest of the product vocabulary above.

## Open questions

- Connected/settle honesty (no stale Connect after success; settle never
posting as the signed-in user; no agent 401 after GitHub already
succeeded) stays **target** until CL-6737 and CL-6738 land — see
IMPLEMENTATION.md open questions; do not document those guarantees as
shipped.
- Template-key-only settle (system sender, no agent wake) is shipped;
generic `connections/pending` still wakes the asking agent. A leftover
agent 401 after GitHub already succeeded is still a first-minute bug
— see IMPLEMENTATION.md; do not document that it cannot happen.
- The precise boundary of what Insights surfaces to a non-admin bench
member (all tenant activity vs. only their own) is not spelled out in
`packages/insights`'s own docs as of this writing.
144 changes: 128 additions & 16 deletions apps/web/src/instant-agent-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
import { QueryClient } from "@tanstack/react-query";
import {
CODE_REVIEW_TEMPLATE,
serializeWorkbenchTemplateManifest,
serializeWorkbenchDefinition,
} from "@corbits/workflow-catalog";

import {
Expand All @@ -16,7 +16,7 @@ function newQueryClient(): QueryClient {
});
}

describe("createWorkbenchFromTemplate (CL-6387)", () => {
describe("createWorkbenchFromTemplate", () => {
const realFetch = globalThis.fetch;

afterEach(() => {
Expand Down Expand Up @@ -146,11 +146,7 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
expect(calls.some((call) => call.path.includes("/invite"))).toBe(false);
});

// and left the reviewer roster its greeting promises out of the room
// (every bench looked like every other "New Workbench", and Myra's
// "Three reviewers read every pull request" greeting described a team
// that wasn't there — see `createWorkbenchFromTemplate`'s own doc).
test("picking the code-review template names the bench after it and invites the whole reviewer roster", async () => {
test("picking the code-review definition names the bench after it and invites exactly its three reviewers", async () => {
const navigated: string[] = [];
let nextReviewerId = 0;
const calls = stubFetch((path) => {
Expand All @@ -160,7 +156,7 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
if (path.endsWith("/library/templates/code-review")) {
return json({
id: "code-review",
content: serializeWorkbenchTemplateManifest(CODE_REVIEW_TEMPLATE),
content: serializeWorkbenchDefinition(CODE_REVIEW_TEMPLATE),
});
}
if (path.endsWith("/chat/workbenches")) {
Expand All @@ -182,6 +178,9 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
id: `def-reviewer-${nextReviewerId}`,
});
}
if (path.endsWith("/chat/workbenches/chan-1/onboarding")) {
return json({ id: "msg-onboarding" }, 201);
}
if (path.endsWith("/chat/workbenches/chan-1/invite")) {
return json({ address: "agent:invited", definitionId: "def-reviewer" });
}
Expand Down Expand Up @@ -222,10 +221,7 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
const createAgentCalls = calls.filter((call) =>
call.path.endsWith("/agent-definitions"),
);
const reviewerCount = CODE_REVIEW_TEMPLATE.participants.filter(
(participant) => participant.handle !== "myra",
).length;
expect(createAgentCalls).toHaveLength(reviewerCount);
expect(createAgentCalls).toHaveLength(CODE_REVIEW_TEMPLATE.agents.length);

const inviteCalls = calls.filter((call) =>
call.path.endsWith("/chat/workbenches/chan-1/invite"),
Expand All @@ -236,7 +232,37 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
const createdIds = createAgentCalls.map(
(_, index) => `def-reviewer-${index + 1}`,
);
expect(invitedIds.sort()).toEqual(["def-assistant", ...createdIds].sort());
expect(invitedIds.sort()).toEqual([...createdIds].sort());
expect(invitedIds).not.toContain("def-assistant");

const settingsBody = JSON.parse(
String(
calls.find((call) =>
call.path.endsWith("/chat/workbenches/chan-1/settings"),
)?.init?.body,
),
);
expect(settingsBody).toEqual({
"template/id": "code-review",
"template/pendingConnections": ["github"],
});

const onboardingCall = calls.find((call) =>
call.path.endsWith("/chat/workbenches/chan-1/onboarding"),
);
expect(JSON.parse(String(onboardingCall?.init?.body))).toEqual({
kind: "connect-github",
requiredForTemplate: "Code review",
promise: CODE_REVIEW_TEMPLATE.promise,
steps: CODE_REVIEW_TEMPLATE.onboardingSteps.map(({ title, why }) => ({
title,
why,
})),
});

const createBodyParsed = JSON.parse(String(createCall?.init?.body));
expect(createBodyParsed.kind).toBe("workbench");
expect(createBodyParsed.definitionId).toBeUndefined();
expect(navigated).toEqual(["/w/chan-1"]);

// CL-6594: a room this function navigates to must never carry a
Expand Down Expand Up @@ -293,7 +319,6 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
"blank",
(to) => navigated.push(to),
newQueryClient(),
undefined,
"Plan the Q3 launch",
);

Expand All @@ -315,7 +340,7 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
if (path.endsWith("/library/templates/code-review")) {
return json({
id: "code-review",
content: serializeWorkbenchTemplateManifest(CODE_REVIEW_TEMPLATE),
content: serializeWorkbenchDefinition(CODE_REVIEW_TEMPLATE),
});
}
if (path.endsWith("/chat/workbenches")) {
Expand All @@ -333,6 +358,9 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
if (path.endsWith("/agent-definitions")) {
return json({ ...assistantDefinitionWire, id: "def-reviewer-1" });
}
if (path.endsWith("/chat/workbenches/chan-1/onboarding")) {
return json({ id: "msg-onboarding" }, 201);
}
if (path.endsWith("/chat/workbenches/chan-1/invite")) {
return json({
address: "agent:invited",
Expand Down Expand Up @@ -366,7 +394,6 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
"code-review",
() => {},
newQueryClient(),
undefined,
"Review the auth PR",
);

Expand All @@ -383,4 +410,89 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => {
),
).toBe(false);
});

// GitHub already connected is not a different create path: the in-room
// card reads live connected state and flips itself to repo pick, so the
// create flow posts the same walkthrough card and never picks repos or
// starts reviewing on the person's behalf.
test("with GitHub already connected, create posts the same walkthrough and never starts reviewing itself", async () => {
const calls = stubFetch((path) => {
if (path.includes("/workflows/definitions")) {
return json({ data: [assistantDefinitionWire], nextCursor: null });
}
if (path.endsWith("/library/templates/code-review")) {
return json({
id: "code-review",
content: serializeWorkbenchDefinition(CODE_REVIEW_TEMPLATE),
});
}
if (path.endsWith("/chat/workbenches")) {
return json({
id: "chan-1",
title: "Code review",
kind: "workbench",
pinned: false,
participants: [],
});
}
if (path.endsWith("/template-blocks/code-review/deploy")) {
return json({ id: "def-code-review-block", created: true });
}
if (path.endsWith("/agent-definitions")) {
return json({ ...assistantDefinitionWire, id: "def-reviewer-1" });
}
if (path.endsWith("/chat/workbenches/chan-1/onboarding")) {
return json({ id: "msg-onboarding" }, 201);
}
if (path.endsWith("/chat/workbenches/chan-1/invite")) {
return json({ address: "agent:invited", definitionId: "def-reviewer" });
}
if (path.endsWith("/chat/workbenches/chan-1/settings")) {
return json({
id: "chan-1",
title: "Code review",
kind: "workbench",
pinned: false,
participants: [],
settings: {},
contextWindow: { value: 0, source: "inherit" },
});
}
if (path.includes("/credentials/resolve/")) {
return json({
id: "cred_github",
tenantId: "tnt_1",
name: "GitHub",
status: "active",
});
}
throw new Error(`unexpected fetch: ${path}`);
});

await createWorkbenchFromTemplate(
"tnt_1",
"code-review",
() => undefined,
newQueryClient(),
);

const onboardingCall = calls.find((call) =>
call.path.endsWith("/chat/workbenches/chan-1/onboarding"),
);
expect(JSON.parse(String(onboardingCall?.init?.body))).toEqual({
kind: "connect-github",
requiredForTemplate: "Code review",
promise: CODE_REVIEW_TEMPLATE.promise,
steps: CODE_REVIEW_TEMPLATE.onboardingSteps.map(({ title, why }) => ({
title,
why,
})),
});
expect(calls.some((call) => call.path.includes("/github/state"))).toBe(
false,
);
expect(
calls.some((call) => call.path.includes("/github/start-reviewing")),
).toBe(false);
});
});
Loading
Loading