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
50 changes: 50 additions & 0 deletions .changeset/home-card-ai-assistant-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
'hotcrm': patch
---

The home page's AI card no longer names a retired assistant persona, and it now
sends you to the entry point the product documentation describes.

The right-hand card on the Sales Home page read:

```
Today with Copilot
Ask the Sales Copilot
Open the floating Copilot (bottom-right) and ask "what should I focus on
today?" — it sees your live pipeline, schema, and accounts.
```

Two things were wrong with that, and both were visible to a user today — this is
interface copy, not documentation. **The persona does not exist**: the app's own
`sales_copilot` agent was retired long ago, and AI capability is implemented by
agents on the platform side while HotCRM contributes domain skills, so a card
inviting you to "ask the Sales Copilot" names an entity this app does not
contain. **The entry point was wrong too**: the card described a floating widget
in the bottom-right corner, while the assistant is the chat panel the platform
opens from the right edge of every page — the wording the `AI Copilot` docs
section already uses. Whichever of the two was accurate, users were being given
two different places to look.

The card now reads:

```
Today with the AI Assistant
Ask the AI Assistant
Open the assistant panel from the right edge of the page and ask "what should I
focus on today?" — it sees your live pipeline, schema, and accounts.
```

What the card *does* is unchanged — same card, same position on the page, same
suggested question, same statement about what the assistant can see. Only the
name and the directions changed.

This was the last live persona mention outside the documentation. The prose
sweep shipped separately and its guard is scoped to the documentation tree, so
this string sat outside every check: `os validate` and `pnpm lint` walk metadata
shape and treat a card's `title` and `description` as free text. A pin in
`test/metadata-references.test.ts` now holds this card to the platform-assistant
wording. Note that card-level copy has no locale keys — the translation contract
carries page `label` / `description` / `title` / `subtitle` only — so this card
renders the English string in every locale, exactly as it did before (#1004).

Fixes #1002.
13 changes: 10 additions & 3 deletions src/pages/home.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,18 @@ export const SalesHomePage: Page = {
{
type: 'page:card',
id: 'ai_briefing',
label: 'Today with Copilot',
// The assistant is the PLATFORM's (`ask`), not an app-owned persona:
// HotCRM contributes skills, the agent lives in the cloud side
// (maintainer ruling on #612, 2026-08-04). The retired `sales_copilot`
// agent (#512, ADR-0063 §2) must not be named in live UI copy — and
// the entry point is the assistant panel the platform opens from the
// right edge of every page, which is the wording
// `content/docs/ai-copilot/index.mdx` landed in #611/PR #1001.
label: 'Today with the AI Assistant',
properties: {
title: 'Ask the Sales Copilot',
title: 'Ask the AI Assistant',
description:
'Open the floating Copilot (bottom-right) and ask "what should I focus on today?" — it sees your live pipeline, schema, and accounts.',
'Open the assistant panel from the right edge of the page and ask "what should I focus on today?" — it sees your live pipeline, schema, and accounts.',
bordered: true,
},
},
Expand Down
80 changes: 80 additions & 0 deletions test/metadata-references.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,3 +746,83 @@ describe('app AI bindings resolve to a platform agent', () => {
expect(((stack as any).skills ?? []).length, 'no skills registered').toBeGreaterThan(0);
});
});

/**
* The home page's AI card does not name a retired persona (#1002).
*
* The two guards above check agent BINDINGS — `defaultAgent`, `stack.agents` —
* because those are keys a schema can be pointed at. This one checks a
* paragraph, and that is the whole point: the card below said
* "Ask the Sales Copilot" for months while every gate stayed green. `os
* validate` and `pnpm lint` walk authored metadata and treat a card's `title` /
* `description` as free text; PR #1001's persona rule is deliberately scoped to
* `content/docs/**` (the range the maintainer drew for #612), so it never
* opened a `.page.ts`. A retired persona in `src/` had no guard at all — and
* this one was live UI copy, visible to a user today, not documentation.
*
* Two drifts were fixed here, both from the maintainer's 2026-08-04 ruling on
* #612 (Option A) and the architecture note that followed it:
*
* - **The persona.** `sales_copilot` was retired in #512 and ADR-0063 §2 made
* the surface skills-only. AI capability is implemented by agents in
* `objectstack-ai/cloud`; HotCRM contributes domain skills. So the copy names
* the platform's assistant, never an app-owned one.
* - **The entry point.** The card sent users to "the floating Copilot
* (bottom-right)"; `content/docs/ai-copilot/index.mdx` documents "the chat
* panel the platform opens from the right edge of every page" (#611 / PR
* #1001). Two descriptions of one entry point is drift whichever is right, so
* the card now uses the documented one.
*
* SCOPE — read before extending. This pins the ONE card this PR rewrote. It is
* not the general rule, and it cannot catch the same persona reappearing in a
* different card, view, or skill description: that is a scan-surface question
* over every user-visible string in `src/` (`title` / `label` / `description` /
* `help`), which touches the public wording surface and needs its own ruling.
* Filed as #1003 for that reason — do not quietly grow this into it.
*
* Reverse verification: predicted red-before / green-after, the ordinary
* direction for a forbidden-string pin over copy that plainly contained the
* string. Measured by restoring the old three lines: 2 of 3 assertions fail
* ("Ask the Sales Copilot" on `title`, "Today with Copilot" on `label`, and the
* floating/bottom-right entry point), then green once rewritten.
*/
describe('live UI copy does not name a retired copilot persona (#1002)', () => {
const homePage = pages.find((p) => p.name === 'sales_home_page');

/** The card carries the app's only AI-facing home-page copy. */
const card = [...walk(homePage?.regions), ...walk(homePage?.slots)].find(
(c: AnyRec) => c.id === 'ai_briefing',
);

it('the card this rule reads is still on the home page', () => {
// Guard the guard: renamed away, every assertion below would pass by
// reading `undefined` — the empty-pass failure mode a copy rule dies of.
expect(homePage, 'sales_home_page is no longer registered in the stack').toBeDefined();
expect(card, 'no component with id "ai_briefing" on sales_home_page').toBeDefined();
expect(typeof card?.properties?.title).toBe('string');
expect(typeof card?.properties?.description).toBe('string');
});

it('no retired persona name in the card copy', () => {
const copy = [card?.label, card?.properties?.title, card?.properties?.description]
.filter((s): s is string => typeof s === 'string')
.join('\n');
// The bare word is included on purpose: "Copilot" alone reads as an
// app-owned assistant in UI copy, even though the DOCS keep *AI Copilot* as
// a section name (#611's line, which applies to a docs nav, not to a card).
const found = ['Sales Copilot', 'Service Copilot', 'Copilot'].filter((name) =>
copy.includes(name),
);
expect(found, `retired persona named in home-page card copy: ${found.join(', ')}`).toEqual([]);
});

it('the card points at the documented assistant entry point', () => {
const description: string = card?.properties?.description ?? '';
// The stale spelling, not a paraphrase of the new one: pinning the exact
// sentence would fail on any harmless rewording, while "floating" /
// "bottom-right" is precisely the claim that contradicts the docs.
expect(description.toLowerCase()).not.toContain('floating');
expect(description.toLowerCase()).not.toContain('bottom-right');
expect(description).toContain('right edge');
});
});
Loading