From 5b62a3883fcb911f795f1394fc7f7b0fa1ff660b Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:50:41 -0500 Subject: [PATCH] feat(settings): add experimental Simplified English Interactions flag (#10934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .../adapter-utils/src/server-utils.test.ts | 31 +++++++++++++++++++ packages/adapter-utils/src/server-utils.ts | 9 ++++++ packages/shared/src/feature-catalog.ts | 8 +++++ packages/shared/src/types/instance.ts | 7 +++++ packages/shared/src/validators/instance.ts | 1 + .../instance-settings-service.test.ts | 14 +++++++++ server/src/services/heartbeat.ts | 8 ++++- server/src/services/instance-settings.ts | 2 ++ .../InstanceExperimentalSettings.test.tsx | 1 + ui/src/pages/InstanceExperimentalSettings.tsx | 14 +++++++++ 10 files changed, 94 insertions(+), 1 deletion(-) diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts index 6467973ab0..2dd38ccdf4 100644 --- a/packages/adapter-utils/src/server-utils.test.ts +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -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", diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 401ae2c27e..6c51a3cd32 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -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"); diff --git a/packages/shared/src/feature-catalog.ts b/packages/shared/src/feature-catalog.ts index 5b0837e17e..9916d916f6 100644 --- a/packages/shared/src/feature-catalog.ts +++ b/packages/shared/src/feature-catalog.ts @@ -189,6 +189,14 @@ export const INSTANCE_FEATURE_CATALOG: Record { 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); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 91c3668539..6941b33ba8 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -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; diff --git a/server/src/services/instance-settings.ts b/server/src/services/instance-settings.ts index f7a017c5c9..9be8b2e1a0 100644 --- a/server/src/services/instance-settings.ts +++ b/server/src/services/instance-settings.ts @@ -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, diff --git a/ui/src/pages/InstanceExperimentalSettings.test.tsx b/ui/src/pages/InstanceExperimentalSettings.test.tsx index 3e6ae06160..04dc76705c 100644 --- a/ui/src/pages/InstanceExperimentalSettings.test.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.test.tsx @@ -90,6 +90,7 @@ function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload { enableGoalsSidebarLink: false, enableTaskWatchdogs: false, enableServerInfoDebugView: false, + enableSimplifiedEnglishInteractions: false, enableSmokeLab: false, autoRestartDevServerWhenIdle: false, enableIssueGraphLivenessAutoRecovery: false, diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index 40a2a40359..1be545941c 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -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" /> + + toggleMutation.mutate({ enableSimplifiedEnglishInteractions: checked }) + } + disabled={toggleMutation.isPending} + managed={managedKeys.enableSimplifiedEnglishInteractions} + ariaLabel="Toggle simplified english interactions experimental setting" + /> +