fix(chat): tell agents which CLI commands a person runs in their own terminal (#20267)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People talk to agents in chat. The chat guidance in
`server/src/services/agent-conversations.ts` tells every chat agent how
to answer.
> - People often ask a chat agent for a terminal command: create a task,
download a plan, inspect or stop a run, or point the CLI at a company.
> - The guidance did not name the Paperclip CLI. Agents guessed: they
did the work themselves, wrote `curl`, used placeholder company ids, or
invented an API address.
> - The person then has to fix the command, or gets a file in the
agent's workspace instead of on their own machine.
> - This pull request adds one guidance paragraph. It names the
supported `npx paperclipai` commands, asks for real ids from the
conversation, and asks for the person's own Paperclip URL as a
placeholder.
> - The benefit is a command the person can run as it is, with the right
company and the right actor.

## Linked Issues or Issue Description

**What happened**
Three chat evaluations (cli-operations) asked for terminal commands.
"Show me how to point my terminal at the Acorn company." got `context
set` with `<acorn-company-id>` and an invented `http://localhost:3100`,
although the agent can read its company id. "Download this task's plan
from the terminal as a file." got a file saved in the agent's own
workspace and an attachment link, with no terminal command. "Show me the
command to inspect Morgan's last run and stop it if it is still
running." got `curl` with environment variables that exist only in the
agent's runtime.

**Expected behavior**
The agent gives `npx paperclipai` commands that the person runs in their
own terminal. The commands use real ids that the agent can read in the
conversation. The Paperclip address is the placeholder `<your Paperclip
URL>`. The agent says which commands need a board user.

**Steps to reproduce**
1. Open a chat with an agent.
2. Send "Download this task's plan from the terminal as a file."
3. See that the agent saves the file itself and gives no terminal
command.

## What Changed

- `server/src/services/agent-conversations.ts`: add a chat guidance
paragraph for terminal and CLI requests. It names the supported commands
for task creation, document download, run read and cancel, and context
profiles. It asks for one command per line, real ids, and the `<your
Paperclip URL>` placeholder.
- `cli/src/__tests__/chat-cli-guidance.test.ts`: a new test reads the
guidance and resolves every `npx paperclipai` command against the
registered commander commands. It fails on an unknown command, flag or
missing argument.
- `server/src/__tests__/agent-conversations.test.ts`: check that the
paragraph is in the directive and in the chat prompt.

## Verification

- `cd cli && npx vitest run src/__tests__/chat-cli-guidance.test.ts`: 3
passed.
- `cd server && npx vitest run src/__tests__/agent-conversations.test.ts
src/__tests__/board-access-denial.test.ts`: 80 passed.
- The chat evaluations `cli-operations.choose-cli-context`,
`export-document-cli` and `inspect-run-cli` run with this commit on an
evaluation branch. Results are added in a comment.

## Risks

- Low risk. The change adds prompt text and tests. Routes, permissions
and responses do not change.
- The chat prompt is about 300 words longer.
- If a CLI command or flag changes, the new CLI test fails until the
guidance changes too.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code
execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Related: #20029 (terminal sign-in guidance in the API catalog).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 authored and GitHub committed 2026-11-09 09:26:13 -06:00
1 parent f7c85bca18
commit 6db0bb9776
3 files changed
+93

No files matched your search

@@ -0,0 +1,80 @@
import { readFileSync } from "node:fs";
import { Command } from "commander";
import { describe, expect, it } from "vitest";
import { registerClientAuthCommands } from "../commands/client/auth.js";
import { registerCompanyCommands } from "../commands/client/company.js";
import { registerContextCommands } from "../commands/client/context.js";
import { registerIssueCommands } from "../commands/client/issue.js";
import { registerRunCommands } from "../commands/client/run.js";
// Chat agents copy these commands into their answers verbatim, so every
// command and flag the chat guidance names must exist in this CLI.
const guidance = readFileSync(new URL("../../../server/src/services/agent-conversations.ts", import.meta.url), "utf8");
function cliProgram() {
const program = new Command();
registerContextCommands(program);
registerIssueCommands(program);
registerCompanyCommands(program);
registerRunCommands(program.command("run"));
registerClientAuthCommands(program.command("auth"));
return program;
}
/** Resolves the command at the start of a guidance sentence; returns its path and the unknown part, if any. */
function resolve(program: Command, text: string): { path: string; error?: string } {
const tokens = (text.match(/<[^<>]+>|\S+/g) ?? []).map(token => token.replace(/[.,;:]$/, ""));
const path: string[] = [];
const result = (error?: string) => ({ path: path.join(" "), error });
let command = program, index = 0;
while (index < tokens.length) {
const next = command.commands.find(child => child.name() === tokens[index]);
if (!next) break;
command = next; path.push(next.name()); index += 1;
}
if (command === program) return result(`unknown command: ${tokens[0]}`);
if (command.commands.length > 0) return result(`unknown subcommand ${tokens[index]} of ${command.name()}`);
let argumentsLeft = command.registeredArguments.length;
const given = new Set<string>();
while (index < tokens.length) {
const token = tokens[index]!;
if (token.startsWith("--")) {
const option = command.options.find(row => row.long === token);
if (!option) return result(`unknown flag ${token} for ${command.name()}`);
given.add(token);
if (option.required || option.optional) {
const value = tokens[index + 1];
if (option.required && (!value || value.startsWith("-"))) return result(`missing value for ${token}`);
index += value && !value.startsWith("-") ? 2 : 1;
} else index += 1;
} else if (token.startsWith("<") && argumentsLeft > 0) {
argumentsLeft -= 1; index += 1;
} else break;
}
if (argumentsLeft > 0) return result(`missing arguments for ${command.name()}`);
const missing = command.options.find(option => option.mandatory && !given.has(option.long!));
return result(missing ? `missing required ${missing.long} for ${command.name()}` : undefined);
}
describe("chat CLI guidance", () => {
const program = cliProgram();
const commands = guidance.split("npx paperclipai ").slice(1).map(rest => resolve(program, rest.slice(0, 400)));
it("names the commands a person runs for tasks, documents, runs and context", () => {
const paths = commands.map(row => row.path);
for (const command of ["issue create", "issue document:get", "run get", "run cancel", "context set", "context show", "company current", "auth whoami"])
expect(paths).toContain(command);
});
it("names only commands, arguments and flags this CLI supports", () => {
for (const command of commands) expect(command.error, command.path).toBeUndefined();
});
it("detects an invented command or flag", () => {
expect(resolve(program, "run cancel <runId> --force now").error).toMatch(/unknown flag --force/);
expect(resolve(program, "context select <name>").error).toMatch(/unknown subcommand select/);
expect(resolve(program, "issue document:get <task> and more").error).toMatch(/missing arguments/);
expect(resolve(program, "context set --profile --use").error).toMatch(/missing value for --profile/);
expect(resolve(program, "issue create --project-id <projectId> creates").error).toMatch(/missing required --company-id/);
});
});
@@ -1005,6 +1005,17 @@ describe("chat prompt policy", () => {
expect(prompt).toContain("GET /api/teams/catalog");
});
it("tells chat to answer terminal requests with the person's own CLI commands", () => {
expect(AGENT_CHAT_DIRECTIVE).toContain("they mean their own terminal: give them commands using the Paperclip CLI, npx paperclipai");
expect(AGENT_CHAT_DIRECTIVE).toContain("instead of doing the work yourself, saving a file in your workspace or writing curl");
expect(AGENT_CHAT_DIRECTIVE).toContain("Fill in values you can read here, such as this company's id");
expect(AGENT_CHAT_DIRECTIVE).toContain("Write the Paperclip address as <your Paperclip URL>");
expect(AGENT_CHAT_DIRECTIVE).toContain("npx paperclipai run cancel <runId> stops a queued or running one (board users only)");
expect(AGENT_CHAT_DIRECTIVE).toContain("a command whose output is piped cannot open the sign-in prompt");
const prompt = buildPaperclipTaskMarkdown({ issue: { id: "chat", identifier: "RUN-1", title: "Chat", workMode: "ask", conversationAgentId: "agent" } });
expect(prompt).toContain("npx paperclipai issue document:get <task id or identifier> <key> --json | jq -r .body > <file name>");
});
it("tells chat where a person can hire when the agent cannot", () => {
expect(AGENT_CHAT_DIRECTIVE).toContain("send the person to the hire form at /<prefix>/agents/new");
expect(AGENT_CHAT_DIRECTIVE).toContain("to your own /permissions page, where they can turn on hiring for you");
@@ -125,6 +125,8 @@ Some developer labs, including the task-chat interaction lab at /<prefix>/dev/ta
Ready-made teams are installable packages in Paperclip's teams catalog, not agents already in this company. When the person asks whether a ready-made team exists, or refers to one, list the catalog with GET /api/teams/catalog and choose by its descriptions and roles, since teams are not named after every kind of project. When you recommend a team, name its roles (agentSlugs), every required skill (requiredSkills, by its ref) and every configuration input (envInputs), and say that a catalog listing does not mean the team's accounts are configured. The catalog is the only source of ready-made teams: do not look for others in package registries, repositories or the web. A team chosen earlier in this chat may be recorded in its task context (get_task_context). POST /api/companies/{companyId}/teams/catalog/{catalogId}/preview shows what an install would add and creates no resources. Installing needs a board user or an agent with permission to create agents. The web app has no teams catalog page, so do not link one: name the team and its id, and when you cannot install, tell the person that a board user can run paperclipai teams install <team id> --company-id <companyId>. After an install, link the installed agents and projects at their own pages.
When the person asks for a terminal or CLI command, or to do something from the terminal, they mean their own terminal: give them commands using the Paperclip CLI, npx paperclipai, instead of doing the work yourself, saving a file in your workspace or writing curl. Put them in one fenced shell block, one complete command per line in the order to run, with no shell conditionals or alternative versions; say in words when a later command depends on an earlier result. The CLI acts as whoever that terminal is signed in as, normally the person's own Board identity, not you, so a control your credential is denied can still be theirs: say which commands need a board user. When the person may not have signed that terminal in, say in a sentence to run npx paperclipai auth login --api-base <your Paperclip URL> first, because a command whose output is piped cannot open the sign-in prompt; a PAPERCLIP_API_KEY set in that terminal, such as an agent key, is used instead of their sign-in, and npx paperclipai auth whoami shows who the commands act as. Fill in values you can read here, such as this company's id and task, agent, project and run ids, instead of placeholders. Write the Paperclip address as <your Paperclip URL>, the address the person opens Paperclip at; do not look it up. Supported commands: npx paperclipai issue create --company-id <companyId> --title <title> --project-id <projectId> --assignee-agent-id <agentId> creates a task. npx paperclipai issue document:get <task id or identifier> <key> --json | jq -r .body > <file name> saves a task document, such as plan, as a file. npx paperclipai run get <runId> reads a run, and npx paperclipai run cancel <runId> stops a queued or running one (board users only). npx paperclipai context set --profile <name> --api-base <your Paperclip URL> --company-id <companyId> --use saves and selects a named profile without changing the person's other profiles, and npx paperclipai context show, npx paperclipai company current and npx paperclipai auth whoami confirm the selected company and identity before any write. Do not invent other flags.
This conversation and your credentials belong to one company. A task identifier whose prefix differs from this conversation's prefix belongs to another company or to none, and you cannot read it here, even when the person can open it themselves: a lookup that is not found does not mean the task does not exist. Do not search for it further, guess its content or copy anything from it. Say that this chat can only work in this company, and that to use that task the person can switch to its company and open a chat there. Link the task at /<its prefix>/issues/<identifier>, taking the prefix from the identifier the person gave; this is the one link that does not use this conversation's prefix.
Tasks and projects in this company can be private. A private task is hidden from you unless you have access to it: your searches, lists and direct lookups leave it out or return not found, even when the person who asks can open it. So when the person refers to a task in this company that one search does not find, especially one they call private, it may be private rather than missing. Do not search for it further through other tools, endpoints, files or activity records, and do not guess its content. Say that this chat cannot see that task, possibly because it is private, and that if they want you to work on it they can open the task and share it with you from its sharing controls, or paste the parts they want here.