feat(settings): add experimental Simplified English Interactions flag (#10934)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents ask humans for decisions through interaction blocks: plan
confirmations, structured questions, suggested tasks, and checkbox
prompts
> - Each agent writes these decision prompts in its own style, so
operators can get long or unclear text at the exact moment they must
decide
> - There is no instance-level control that makes agents use a
controlled language for these decision points only
> - This pull request adds an experimental setting that tells agents to
write all user-interaction content in ASD-STE100 Simplified Technical
English, with the context the user needs and the effect of each choice
> - The benefit is faster, clearer human decisions, with agent thinking
and normal responses unchanged

## Linked Issues or Issue Description

Refs #10410 (optional `/simplified-english` skill in the skills catalog;
this PR adds the instance-level toggle for interactions).

**Problem or motivation**

Agent-posted user interactions (plan confirmations, structured
questions, suggested-task proposals, checkbox prompts) are written in
each agent's default style. Operators who want fast, unambiguous
decisions have no way to ask agents to use a controlled language for
exactly those decision points.

**Proposed solution**

Add an experimental instance setting,
`enableSimplifiedEnglishInteractions` ("Simplified English
Interactions"). When it is on, the server sets
`simplifiedEnglishInteractions: true` in the heartbeat wake payload. The
shared wake-prompt renderer, used by every adapter, then emits a
directive: write all user-interaction content in ASD-STE100 Simplified
Technical English, state what information the user needs to decide, and
state what happens for each choice. The directive applies to interaction
content only. Thinking, comments, documents, and other responses keep
their usual style.

**Alternatives considered**

Per-agent instructions work today, but someone must maintain them on
every agent. An instance-level toggle applies uniformly and turns off in
one place. Server-side rewriting of interaction payloads was rejected:
post-hoc translation is lossy and cannot add the decision context that
only the agent has.

**Roadmap alignment**

Extends the experimental settings surface with another opt-in
agent-behavior refinement, consistent with existing prompt-side flags.

## What Changed

- Added `enableSimplifiedEnglishInteractions` to the experimental
instance-settings zod schema, mirror type, and feature catalog (default
off, tier preference) in `packages/shared`.
- Server `instance-settings.ts` normalizes the flag on both read
branches; `heartbeat.ts` reads it once and passes
`simplifiedEnglishInteractions` into `buildPaperclipWakePayload`.
- Shared adapter renderer
(`packages/adapter-utils/src/server-utils.ts`): added the field to
`PaperclipWakePayload`, normalization, and an `- interaction language
(experimental): ...` directive emitted in both fresh and resume prompt
lanes, so one injection point covers all adapters.
- UI: new experimental settings card "Simplified English Interactions"
in `ui/src/pages/InstanceExperimentalSettings.tsx`, in alphabetical card
order; fixtures updated.
- Tests: renderer coverage for flag on/off in both lanes, plus
schema/catalog/UI fixture updates.

## Verification

- From the repo root: `node_modules/.bin/vitest run
packages/adapter-utils` (88/88), `packages/shared` validators (25/25),
server instance-settings + heartbeat suites (47/47 and 70/70 consumer
tests), `ui` settings tests (32/32).
- Typecheck is clean in all four touched packages.
- Manual check: turn the flag on in Settings → Experimental, wake an
agent, and confirm the wake prompt contains the interaction-language
directive; turn it off and confirm the directive is absent.

## Risks

- Low risk: the flag defaults to off, and the only behavior change is
one extra directive line in the wake prompt when an operator turns it
on.
- The directive is advisory to the agent; models can still deviate from
STE. No data or API shape changes; no migration.

## Model Used

- Claude Fable 5 (Anthropic, model ID `claude-fable-5`), extended
thinking, agentic tool use via Claude Agent SDK.

## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-08-05 21:50:41 -05:00 committed by GitHub
parent e43f187cad
commit 5b62a3883f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 94 additions and 1 deletions

View File

@ -742,6 +742,37 @@ describe("renderPaperclipWakePrompt", () => {
);
});
it("renders the simplified-english interaction directive only when the payload enables it", () => {
const payload = {
reason: "issue_commented",
issue: {
id: "issue-1",
identifier: "PAP-15936",
title: "Interaction language",
description: null,
descriptionTruncated: false,
status: "in_progress",
},
commentWindow: { requestedCount: 0, includedCount: 0, missingCount: 0 },
comments: [],
fallbackFetchNeeded: false,
};
expect(renderPaperclipWakePrompt(payload)).not.toContain("ASD-STE100");
const enabled = { ...payload, simplifiedEnglishInteractions: true };
const fresh = renderPaperclipWakePrompt(enabled);
expect(fresh).toContain("ASD-STE100 Simplified Technical English");
expect(fresh).toContain("what happens for each choice");
// Resume deltas carry the directive too: the setting can change between wakes.
expect(renderPaperclipWakePrompt(enabled, { resumedSession: true })).toContain(
"ASD-STE100 Simplified Technical English",
);
expect(JSON.parse(stringifyPaperclipWakePayload(enabled) ?? "{}")).toMatchObject({
simplifiedEnglishInteractions: true,
});
});
it("suppresses the issue description when the prompt already carries the task-context markdown", () => {
const payload = {
reason: "issue_assigned",

View File

@ -666,6 +666,9 @@ type PaperclipWakePayload = {
recovery: PaperclipWakeRecovery | null;
issue: PaperclipWakeIssue | null;
checkedOutByHarness: boolean;
// Experimental: write user-interaction content in ASD-STE100 Simplified
// Technical English with brief decision context.
simplifiedEnglishInteractions: boolean;
dependencyBlockedInteraction: boolean;
treeHoldInteraction: boolean;
activeTreeHold: PaperclipWakeTreeHoldSummary | null;
@ -1317,6 +1320,7 @@ export function normalizePaperclipWakePayload(value: unknown): PaperclipWakePayl
recovery,
issue: normalizePaperclipWakeIssue(payload.issue),
checkedOutByHarness: asBoolean(payload.checkedOutByHarness, false),
simplifiedEnglishInteractions: asBoolean(payload.simplifiedEnglishInteractions, false),
dependencyBlockedInteraction: asBoolean(payload.dependencyBlockedInteraction, false),
treeHoldInteraction: asBoolean(payload.treeHoldInteraction, false),
activeTreeHold,
@ -1624,6 +1628,11 @@ export function renderPaperclipWakePrompt(
`- execution workspace branch: you are running in an execution workspace on branch ${markdownInlineCode(normalized.executionWorkspace.branchName)}. Do not switch, rename, or re-point this branch; keep all commits on it.`,
);
}
if (normalized.simplifiedEnglishInteractions) {
lines.push(
"- interaction language (experimental): write every user interaction you post (request_confirmation, ask_user_questions, suggest_tasks, checkbox prompts and options, and any other content rendered inside an interaction block) in ASD-STE100 Simplified Technical English. In each interaction, briefly tell the user what information they need to make the decision and what happens for each choice. This applies only to interaction content — write your thinking, comments, documents, and other responses in your usual style.",
);
}
if (normalized.dependencyBlockedInteraction) {
lines.push("- dependency-blocked interaction: yes");
lines.push("- execution scope: respond or triage the human comment; do not treat blocker-dependent deliverable work as unblocked");

View File

@ -189,6 +189,14 @@ export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalog
cloudDefault: false,
selfHostedDefault: false,
},
enableSimplifiedEnglishInteractions: {
title: "Simplified English Interactions",
description:
"Instruct agents to write user interactions (confirmations, questions, suggested tasks) in ASD-STE100 Simplified Technical English with brief decision context.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableServerInfoDebugView: {
title: "Server Info Debug View",
description:

View File

@ -65,6 +65,13 @@ export interface InstanceExperimentalSettings {
enableDecisions: boolean;
enableGoalsSidebarLink: boolean;
enableServerInfoDebugView: boolean;
/**
* Instructs agents to write user-interaction content (confirmations,
* questions, suggested tasks, checkbox prompts) in ASD-STE100 Simplified
* Technical English with brief decision context. Prompt-side only; no
* behavior change outside interaction wording.
*/
enableSimplifiedEnglishInteractions: boolean;
autoRestartDevServerWhenIdle: boolean;
enableIssueGraphLivenessAutoRecovery: boolean;
enableWorkspaceBranchReconcileForward: boolean;

View File

@ -59,6 +59,7 @@ export const instanceExperimentalSettingsSchema = z.object({
enableDecisions: z.boolean().default(false),
enableGoalsSidebarLink: z.boolean().default(false),
enableServerInfoDebugView: z.boolean().default(false),
enableSimplifiedEnglishInteractions: z.boolean().default(false),
autoRestartDevServerWhenIdle: z.boolean().default(false),
enableIssueGraphLivenessAutoRecovery: z.boolean().default(false),
enableWorkspaceBranchReconcileForward: z.boolean().default(true),

View File

@ -44,6 +44,7 @@ describe("instance settings service", () => {
enableDecisions: false,
enableGoalsSidebarLink: true,
enableServerInfoDebugView: true,
enableSimplifiedEnglishInteractions: false,
autoRestartDevServerWhenIdle: true,
enableIssueGraphLivenessAutoRecovery: true,
enableWorkspaceBranchReconcileForward: true,
@ -71,6 +72,19 @@ describe("instance settings service", () => {
).toBe(false);
});
it("defaults enableSimplifiedEnglishInteractions to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableSimplifiedEnglishInteractions).toBe(false);
expect(normalizeExperimentalSettings({}).enableSimplifiedEnglishInteractions).toBe(false);
expect(
normalizeExperimentalSettings({ enableStreamlinedLeftNavigation: true })
.enableSimplifiedEnglishInteractions,
).toBe(false);
expect(
normalizeExperimentalSettings({ enableSimplifiedEnglishInteractions: true })
.enableSimplifiedEnglishInteractions,
).toBe(true);
});
it("defaults enableTaskWatchdogs to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableTaskWatchdogs).toBe(false);
expect(normalizeExperimentalSettings({}).enableTaskWatchdogs).toBe(false);

View File

@ -5464,6 +5464,9 @@ export async function buildPaperclipWakePayload(input: {
}
| null;
exposeLowTrustRaw?: boolean;
// Experimental: agents write user-interaction content in ASD-STE100
// Simplified Technical English (rendered as a prompt directive downstream).
simplifiedEnglishInteractions?: boolean;
}) {
const executionStage = parseObject(input.contextSnapshot.executionStage);
const commentIds = extractWakeCommentIds(input.contextSnapshot);
@ -5739,6 +5742,7 @@ export async function buildPaperclipWakePayload(input: {
interactionStatus,
checkboxSelection: Object.keys(checkboxSelection).length > 0 ? checkboxSelection : null,
checkedOutByHarness: input.contextSnapshot[PAPERCLIP_HARNESS_CHECKOUT_KEY] === true,
simplifiedEnglishInteractions: input.simplifiedEnglishInteractions === true,
dependencyBlockedInteraction: input.contextSnapshot.dependencyBlockedInteraction === true,
treeHoldInteraction: input.contextSnapshot.treeHoldInteraction === true,
activeTreeHold: parseObject(input.contextSnapshot.activeTreeHold),
@ -13576,7 +13580,8 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
issueContext.assigneeAdapterOverrides,
)
: null;
const isolatedWorkspacesEnabled = (await instanceSettings.getExperimental()).enableIsolatedWorkspaces;
const experimentalInstanceSettings = await instanceSettings.getExperimental();
const isolatedWorkspacesEnabled = experimentalInstanceSettings.enableIsolatedWorkspaces;
const parsedIssueExecutionWorkspaceSettings = parseIssueExecutionWorkspaceSettings(
issueContext?.executionWorkspaceSettings,
);
@ -13778,6 +13783,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
}
: null,
exposeLowTrustRaw,
simplifiedEnglishInteractions: experimentalInstanceSettings.enableSimplifiedEnglishInteractions === true,
});
if (paperclipWakePayload) {
context[PAPERCLIP_WAKE_PAYLOAD_KEY] = paperclipWakePayload;

View File

@ -227,6 +227,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableDecisions: parsed.data.enableDecisions ?? false,
enableGoalsSidebarLink: parsed.data.enableGoalsSidebarLink ?? false,
enableServerInfoDebugView: parsed.data.enableServerInfoDebugView ?? false,
enableSimplifiedEnglishInteractions: parsed.data.enableSimplifiedEnglishInteractions ?? false,
autoRestartDevServerWhenIdle: parsed.data.autoRestartDevServerWhenIdle ?? false,
enableIssueGraphLivenessAutoRecovery: parsed.data.enableIssueGraphLivenessAutoRecovery ?? false,
enableWorkspaceBranchReconcileForward: parsed.data.enableWorkspaceBranchReconcileForward ?? true,
@ -262,6 +263,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableDecisions: false,
enableGoalsSidebarLink: false,
enableServerInfoDebugView: false,
enableSimplifiedEnglishInteractions: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: false,
enableWorkspaceBranchReconcileForward: true,

View File

@ -90,6 +90,7 @@ function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload {
enableGoalsSidebarLink: false,
enableTaskWatchdogs: false,
enableServerInfoDebugView: false,
enableSimplifiedEnglishInteractions: false,
enableSmokeLab: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: false,

View File

@ -379,6 +379,8 @@ export function InstanceExperimentalSettings() {
const enableGoalsSidebarLink = experimentalQuery.data?.enableGoalsSidebarLink === true;
const enableCases = experimentalQuery.data?.enableCases === true;
const enableServerInfoDebugView = experimentalQuery.data?.enableServerInfoDebugView === true;
const enableSimplifiedEnglishInteractions =
experimentalQuery.data?.enableSimplifiedEnglishInteractions === true;
const enableSmokeLab = experimentalQuery.data?.enableSmokeLab === true;
const autoRestartDevServerWhenIdle = experimentalQuery.data?.autoRestartDevServerWhenIdle === true;
const enableIssueGraphLivenessAutoRecovery =
@ -748,6 +750,18 @@ export function InstanceExperimentalSettings() {
ariaLabel="Toggle server info debug view experimental setting"
/>
<ExperimentalToggleCard
title="Simplified English Interactions"
description="Instruct agents to write user interactions (plan confirmations, questions, suggested tasks, checkbox prompts) in ASD-STE100 Simplified Technical English, with brief context on what information the decision needs and what happens for each choice."
checked={enableSimplifiedEnglishInteractions}
onCheckedChange={(checked) =>
toggleMutation.mutate({ enableSimplifiedEnglishInteractions: checked })
}
disabled={toggleMutation.isPending}
managed={managedKeys.enableSimplifiedEnglishInteractions}
ariaLabel="Toggle simplified english interactions experimental setting"
/>
<ExperimentalToggleCard
title="Smoke Lab"
description='Add a "Smoke Lab" tab under Apps → Developer and an "Integration smoke" card on the dashboard for exercising every integration path against deterministic local fixtures (fake OAuth provider + loopback MCP servers). Private (non-public) deployments only.'