Skip to content

sync: paperclip upstream (2026-04-28)#160

Open
TSavo wants to merge 3 commits into
mainfrom
sync/paperclip-upstream-2026-04-28
Open

sync: paperclip upstream (2026-04-28)#160
TSavo wants to merge 3 commits into
mainfrom
sync/paperclip-upstream-2026-04-28

Conversation

@TSavo

@TSavo TSavo commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Automated upstream sync — Paperclip

Synced with latest from paperclipai/paperclip upstream.

What this does

  • Pulls in latest upstream changes (features, bug fixes, refactors)
  • Resolves any merge conflicts (preserving hostedMode guards)
  • Scans for new UI elements that leak infra without hostedMode guards
  • Fixes any gaps found

Verify

  • Build passes
  • hostedMode still hides all infra UI
  • No adapter/model selection visible in hosted mode

Note

Sync paperclip upstream with workflow-sorted sub-issues, stalled blocker detection, and issue graph liveness auto-recovery

  • Adds a configurable issue graph liveness auto-recovery system: a lookback-window-based classifier detects stalled in_review issues lacking an action path, creates recovery issues (deduped via a new partial unique index), and escalates failed recoveries in place. Two new API endpoints (/preview and /run) expose this to the experimental settings UI.
  • Introduces workflow-aware sub-issue sorting (workflowSort) with checklist step numbers, current-step highlighting, dependency chips, and a segmented progress summary strip in IssuesList.
  • Adds stalled blocker detection: IssueBlockerAttention gains stalledBlockerCount/sampleStalledBlockerIdentifier; StatusIcon renders amber styling for stalled-review blocks; new IssueBlockedNotice component shows stalled-in-review rows.
  • Adds pause/resume controls to sidebar agent rows via a new SidebarAgentItem actions dropdown, with optimistic pending state and toast feedback.
  • Changes requireBoardApprovalForNewAgents default to false in schema, migration, portability service, and exported manifests.
  • Consolidates adapter workspace env-var setup into a shared applyPaperclipWorkspaceEnv utility; adds a filesystem-lock build script to serialize plugin SDK dependency compilation.
  • Fixes parseIssuePathIdFromPath and parseMentionChipHref to return null for absolute http(s):// URLs instead of treating them as internal references.
  • Risk: requireBoardApprovalForNewAgents default flip affects all new companies and existing exports that omit the field — imports now default to false.

Macroscope summarized 366a5c1.

Summary by CodeRabbit

  • New Features

    • Added issue graph liveness auto-recovery with configurable lookback period and preview capability.
    • Introduced sub-issue workflow sorting with progress summary and checklist-style UI.
    • Added detection for stalled review blockers with distinct visual indicators.
    • Added AWS ECS Fargate deployment guide with complete provisioning steps.
    • New "Security" agent role support.
    • Agent pause/resume capability with UI controls.
  • Bug Fixes

    • Fixed port rewriting logic for local development.
    • Improved recovery issue creation race condition handling.
  • Database Migrations

    • Changed hire approval default to disabled for new companies.
    • Added uniqueness constraint for active recovery issues.

github-actions Bot and others added 3 commits April 28, 2026 08:27
…paperclip with paperclipai/paperclip@d0bdbe11, 27 commits ahead of prior baseline 40782f70.\n\nUpstream-Baseline-Sha: d0bdbe11a9624435b6dca3968389bd59c6a559a2
Add useHostedMode hooks and early redirects to infrastructure-management pages that should not be accessible in hosted mode:
- InstanceAccess: User access management
- AgentDetail: Agent configuration and management
- JoinRequestQueue: Join request approvals

These pages expose infrastructure details like user permissions, agent models, and API configurations that should only be available to self-hosted administrators.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Install missing dependencies to resolve TypeScript compilation errors.
The hostedMode guards are correctly implemented in:
- AgentDetail: Early redirect before useParams hook
- InstanceAccess: Early redirect before useCompany hook
- JoinRequestQueue: Early redirect before useCompany hook

All imports and hook ordering are correct.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 28, 2026 08:36
@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown

Too many files changed for review. (130 files found, 100 file limit)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @TSavo, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces issue-graph liveness auto-recovery with configurable lookback periods, workflow-based sub-issue ordering, stalled-review blocker detection, hire approval policy defaults (false), agent pause/resume UI, security role support, AWS ECS deployment guide, port-rewriting simplification, workspace environment variable consolidation, hosted-mode guards, and comprehensive testing/documentation updates.

Changes

Cohort / File(s) Summary
Upstream & Release
sidecars/paperclip/.upstream-baseline, sidecars/paperclip/releases/v2026.427.0.md
Updates upstream baseline commit and adds comprehensive v2026.427.0 release notes documenting multi-user control-plane, structured threads, heartbeat liveness, ordered sub-issues, subtree operations, cross-issue references, beta environments, and scheduling improvements.
Community Links & Documentation
sidecars/paperclip/README.md, sidecars/paperclip/cli/README.md, sidecars/paperclip/doc/SPEC-implementation.md, sidecars/paperclip/doc/execution-semantics.md
Adds Twitter/X social links to README files; updates execution semantics docs with explicit non-terminal liveness rules and stalled-review classification; adds formal recovery behavior contracts.
AWS ECS Deployment
sidecars/paperclip/docker/ecs-task-definition.json, sidecars/paperclip/docs/deploy/aws-ecs.md, sidecars/paperclip/docs/deploy/overview.md
Introduces complete ECS Fargate task definition and end-to-end AWS deployment guide with VPC/security/RDS/EFS/ALB/ACM/scaling setup; updates deployment overview with link.
Delegation & Policy Configuration
sidecars/paperclip/docs/guides/board-operator/delegation.md, sidecars/paperclip/docs/start/core-concepts.md, sidecars/paperclip/packages/db/src/migrations/0071_default_hire_approval_off.sql, sidecars/paperclip/packages/db/src/schema/companies.ts
Updates documentation to reflect conditional hire approval gates rather than fixed approval requirements; sets database default for requireBoardApprovalForNewAgents to false via migration and schema update.
Database Migrations & Schema
sidecars/paperclip/packages/db/src/migrations/0072_large_sandman.sql, sidecars/paperclip/packages/db/src/schema/issues.ts, sidecars/paperclip/packages/db/src/migrations/meta/_journal.json
Adds unique partial index on stranded-issue-recovery origins; updates migration journal with two new migration entries.
Plugin Build & SDK
sidecars/paperclip/packages/plugins/sdk/package.json, sidecars/paperclip/packages/plugins/examples/plugin-*/package.json, sidecars/paperclip/packages/plugins/sandbox-providers/e2b/package.json, sidecars/paperclip/server/package.json, sidecars/paperclip/scripts/ensure-plugin-build-deps.mjs
Consolidates plugin build-dependency setup: all plugin examples/providers now invoke ensure-build-deps from SDK rather than running build or local scripts; updates SDK and scripts to expose/support this pattern with filesystem locking for concurrency.
Agent Roles & Shared Types
sidecars/paperclip/packages/shared/src/constants.ts, sidecars/paperclip/packages/shared/src/adapter-types.test.ts
Adds "security" role to agent role constants and label mappings; includes unit test coverage.
Instance Settings & Auto-Recovery Types
sidecars/paperclip/packages/shared/src/types/instance.ts, sidecars/paperclip/packages/shared/src/types/index.ts, sidecars/paperclip/packages/shared/src/index.ts, sidecars/paperclip/packages/shared/src/validators/instance.ts, sidecars/paperclip/packages/shared/src/validators/index.ts
Introduces issue-graph liveness auto-recovery preview types, lookback-hour constants (default/min/max), validator schemas for recovery requests, and corresponding TypeScript type exports across shared barrel files.
Issue & Blocker Attention Types
sidecars/paperclip/packages/shared/src/types/issue.ts
Adds "stalled" blocker attention state and "stalled_review" reason; adds stalledBlockerCount and sampleStalledBlockerIdentifier to IssueBlockerAttention.
Adapter Workspace Env Consolidation
sidecars/paperclip/packages/adapter-utils/src/server-utils.ts, sidecars/paperclip/packages/adapter-utils/src/server-utils.test.ts, sidecars/paperclip/packages/adapters/claude-local/src/server/execute.ts, sidecars/paperclip/packages/adapters/codex-local/src/server/execute.ts, sidecars/paperclip/packages/adapters/cursor-local/src/server/execute.ts, sidecars/paperclip/packages/adapters/gemini-local/src/server/execute.ts, sidecars/paperclip/packages/adapters/opencode-local/src/server/execute.ts, sidecars/paperclip/packages/adapters/pi-local/src/server/execute.ts
Introduces applyPaperclipWorkspaceEnv helper to centralize workspace environment variable population; refactors all adapter execute functions to delegate conditional env-key setting to this helper; includes unit test coverage.
Skill Discovery & Bundled Skills
sidecars/paperclip/packages/adapter-utils/src/server-utils.ts
Updates listPaperclipSkillEntries to parse required field from SKILL.md frontmatter instead of unconditionally marking all skills as required; sets requiredReason only when required: true.
E2E & Integration Tests
sidecars/paperclip/cli/src/__tests__/company-import-export-e2e.test.ts, sidecars/paperclip/cli/src/__tests__/worktree.test.ts
Refactors CLI env setup to use centralized TestPaperclipEnv/createBasePaperclipEnv; updates runCliJson to dynamically build args and accept options; adds test case validating isolated paperclip-home context isolation; clarifies rewriteLocalUrlPort behavior for non-loopback hosts with ports.
Worktree Port Rewriting
sidecars/paperclip/cli/src/commands/worktree-lib.ts, sidecars/paperclip/server/src/worktree-config.ts
Simplifies rewriteLocalUrlPort logic: removes loopback-hostname filtering; only rewrites when URL has explicit port (not default-port-normalized).
Server Port Detection & Auth Setup
sidecars/paperclip/server/src/auth/better-auth.ts, sidecars/paperclip/server/src/index.ts
Refactors port detection to separate "requested" vs "detected" ports; updates deriveAuthTrustedOrigins to accept listenPort override and conditionally include explicit port entries; moves trusted-origin derivation before Better Auth instantiation; updates rewrite logic to use stable listenPort.
Heartbeat & Recovery Service
sidecars/paperclip/server/src/services/heartbeat.ts, sidecars/paperclip/server/src/services/recovery/issue-graph-liveness.ts, sidecars/paperclip/server/src/services/recovery/origins.ts, sidecars/paperclip/server/src/services/recovery/service.ts, sidecars/paperclip/server/src/services/recovery/index.ts
Adds queued-run staleness detection during claim; introduces buildIssueGraphLivenessAutoRecoveryPreview and expands reconcileIssueGraphLiveness with force/lookbackHours options; adds new "in_review_without_action_path" liveness state; implements stalled-review detection; withholds run failure details in comments; hardens recovery issue race handling; adds isStrandedIssueRecoveryOriginKind predicate; implements configurable lookback-hour gating with cutoff calculation.
Instance Settings & Routines Services
sidecars/paperclip/server/src/services/instance-settings.ts, sidecars/paperclip/server/src/services/routines.ts, sidecars/paperclip/server/src/services/company-skills.ts, sidecars/paperclip/server/src/services/company-portability.ts
Adds lookback-hours normalization to experimental settings; updates routine execution to accept optional actor and associate manual runs with runner; adds inbox-state persistence helpers for routine coalescing; validates company existence in skill refresh; updates portability defaults for hire approval policy.
Issues Service & Blocker Logic
sidecars/paperclip/server/src/services/issues.ts
Adds includeBlockedBy filter to issue listing; extends listIssueBlockerAttentionMap with stalled-review classification (new state/reason); adds pending-indicator queries for in_review blockers (thread interactions, approvals, recovery issues); refactors comment pagination with composable comparisons; tracks stalled blocker counts/samples.
Routes & Endpoints
sidecars/paperclip/server/src/routes/instance-settings.ts, sidecars/paperclip/server/src/routes/issues.ts, sidecars/paperclip/server/src/routes/routines.ts
Adds two new experimental instance-settings endpoints for auto-recovery preview/run with validation and logging; adds includeBlockedBy query param to issues list; updates routine run endpoint to pass actor context to service.
Server Test Fixtures & Mocks
sidecars/paperclip/server/src/__tests__/agent-permissions-routes.test.ts, sidecars/paperclip/server/src/__tests__/agent-skills-routes.test.ts, sidecars/paperclip/server/src/__tests__/better-auth.test.ts, sidecars/paperclip/server/src/__tests__/company-portability.test.ts
Removes default environment/OpenCode stubs; updates agent-skills mocks for persistent agent state; adds trusted-origin coverage including port handling; updates hire-approval defaults and adds export test; revises test expectations for policy defaults.
Heartbeat & Recovery Tests
sidecars/paperclip/server/src/__tests__/heartbeat-dependency-scheduling.test.ts, sidecars/paperclip/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts, sidecars/paperclip/server/src/__tests__/heartbeat-process-recovery.test.ts, sidecars/paperclip/server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts
Adds concurrency limiting test; updates liveness escalation with budget/owner-selection logic; extensively expands recovery tests with error handling, stranded reconciliation, budget blocking, secret redaction, and raced recovery handling; introduces new stale-queue-invalidation suite with issue-state change scenarios.
Instance Settings & Liveness Tests
sidecars/paperclip/server/src/__tests__/instance-settings-routes.test.ts, sidecars/paperclip/server/src/__tests__/issue-liveness.test.ts, sidecars/paperclip/server/src/__tests__/issue-blocker-attention.test.ts
Updates experimental settings fixtures with auto-recovery config; adds endpoint coverage for preview/run; expands liveness classification with blocked-chain/stalled-review detection and owner-candidate selection; adds stalled-review blocker attention test cases.
Issues Service Tests
sidecars/paperclip/server/src/__tests__/issues-service.test.ts, sidecars/paperclip/server/src/__tests__/company-skills-service.test.ts, sidecars/paperclip/server/src/__tests__/recovery-classifiers.test.ts, sidecars/paperclip/server/src/__tests__/routines-routes.test.ts, sidecars/paperclip/server/src/__tests__/routines-service.test.ts, sidecars/paperclip/server/src/__tests__/paperclip-skill-utils.test.ts
Adds comment pagination and blockedBy relation coverage; validates 404 on missing company; tests stranded-recovery classifier; adds manual routine authorization and inbox-state tests; verifies skill frontmatter parsing; cleans up test setup/teardown.
Server Startup & Worktree Tests
sidecars/paperclip/server/src/__tests__/server-startup-feedback-export.test.ts, sidecars/paperclip/server/src/__tests__/worktree-config.test.ts
Adds authenticated auth-origin setup coverage; expands port-rewriting assertions for explicit-port authPublicBaseUrl; updates worktree port persistence with parameterized base URL; removes in-memory-only test and adds focused persistence coverage.
UI API & Type Exports
sidecars/paperclip/ui/src/api/instanceSettings.ts, sidecars/paperclip/ui/src/api/issues.ts, sidecars/paperclip/ui/src/api/issues.test.ts
Adds previewIssueGraphLivenessAutoRecovery and runIssueGraphLivenessAutoRecovery client methods; adds includeBlockedBy filter to issues list; updates test to assert filter in URL.
Issue Detail & Blocker UI Components
sidecars/paperclip/ui/src/components/IssueBlockedNotice.tsx, sidecars/paperclip/ui/src/components/IssueChatThread.tsx, sidecars/paperclip/ui/src/components/IssueProperties.test.tsx
Introduces new IssueBlockedNotice component for amber blocked/stalled panels; extracts from IssueChatThread; adds stalled-blocker reordering and differentiated messaging; threads blockerAttention through chat thread; updates test fixtures.
Issue Row & Column Metadata
sidecars/paperclip/ui/src/components/IssueColumns.tsx, sidecars/paperclip/ui/src/components/IssueRow.tsx, sidecars/paperclip/ui/src/components/IssueRow.test.tsx
Adds checklist-step-number rendering to metadata; adds checklist props to IssueRow (title styling, step number, current-step semantics, dependency chips); includes test coverage.
Sub-Issues & Workflow UI
sidecars/paperclip/ui/src/lib/workflow-sort.ts, sidecars/paperclip/ui/src/lib/workflow-sort.test.ts, sidecars/paperclip/ui/src/lib/issue-detail-subissues.ts, sidecars/paperclip/ui/src/lib/issue-detail-subissues.test.ts, sidecars/paperclip/ui/src/components/IssuesList.tsx, sidecars/paperclip/ui/src/components/IssuesList.test.tsx
Introduces workflow-sort algorithm for deterministic blocker-chain ordering; adds sub-issue progress-summary types/helpers with target selection; extends IssuesList with workflow sort option, progress-summary rendering, checklist metadata (step numbers, current-step styling, dependency chips); comprehensive test coverage for sort behavior and UI rendering.
Issue References & Mentions
sidecars/paperclip/ui/src/lib/issue-reference.ts, sidecars/paperclip/ui/src/lib/issue-reference.test.ts, sidecars/paperclip/ui/src/lib/mention-chips.ts
Updates issue-reference parsing to treat absolute HTTP(S) URLs as non-internal references (returns null instead of extracting/transforming); adds rejection of absolute URLs in mention-chip parsing.
Comment Auto-loading & Documents
sidecars/paperclip/ui/src/lib/optimistic-issue-comments.ts, sidecars/paperclip/ui/src/lib/optimistic-issue-comments.test.ts, sidecars/paperclip/ui/src/components/IssueDocumentsSection.tsx
Adds gating predicate shouldAutoloadOlderIssueComments for chat-tab-only autoloading; includes test coverage; removes document body padding wrappers and unused CSS.
Status & Blocker Attention Rendering
sidecars/paperclip/ui/src/components/StatusIcon.tsx, sidecars/paperclip/ui/src/components/StatusIcon.test.tsx, sidecars/paperclip/ui/storybook/stories/status-language.stories.tsx
Adds stalled-state support to status icon with amber styling and dot overlay; adds stalled-specific aria labels; expands Storybook matrix with stalled cases and fixtur helpers; adds IssueBlockedNotice story section.
Sidebar Agent Actions
sidecars/paperclip/ui/src/components/SidebarAgents.tsx, sidecars/paperclip/ui/src/components/SidebarAgents.test.tsx
Adds per-agent action dropdown with pause/resume mutations; updates agent state after mutations; emits success/error toasts; handles budget-paused state; includes comprehensive test coverage.
Inline Entity Selector
sidecars/paperclip/ui/src/components/InlineEntitySelector.tsx, sidecars/paperclip/ui/src/components/InlineEntitySelector.test.tsx
Synchronizes highlighted index via ref+state to prevent stale closure issues during keyboard navigation; adds jsdom test suite for keyboard interaction and selection.
Issue Detail Page
sidecars/paperclip/ui/src/pages/IssueDetail.tsx, sidecars/paperclip/ui/src/pages/IssueDetail.test.tsx
Adds auto-loading of older comments via shouldAutoloadOlderIssueComments predicate; requests includeBlockedBy for sub-issues; enables workflow sort and progress-summary on sub-issues list; threads blockerAttention into chat UI; updates test expectations.
Markdown & Company Context
sidecars/paperclip/ui/src/components/MarkdownBody.test.tsx, sidecars/paperclip/ui/src/context/CompanyContext.tsx, sidecars/paperclip/ui/src/context/CompanyContext.test.tsx
Updates absolute Paperclip issue URL handling to preserve external links instead of transforming to internal routes; refactors company list query to include unauthorized flag; defers stored-selection clearing until load completes; adds helper functions for bootstrap selection logic; includes comprehensive test suite.
Hosted Mode Guards
sidecars/paperclip/ui/src/pages/AgentDetail.tsx, sidecars/paperclip/ui/src/pages/InstanceAccess.tsx, sidecars/paperclip/ui/src/pages/JoinRequestQueue.tsx
Adds hosted-mode detection guards that redirect to / with replace when isHosted is true, preventing component mount/rendering.
Instance Experimental Settings
sidecars/paperclip/ui/src/pages/InstanceExperimentalSettings.tsx
Expands auto-recovery feature from simple toggle into lookback-configurable workflow with preview dialog, lookback-hours input (1–720), save/preview/run-now actions, and feature-gating.
Storybook Stories
sidecars/paperclip/ui/storybook/stories/sub-issues-workflow.stories.tsx
Introduces new workflow-checklist story with fixture PAP topology, Query cache hydration, workflow sort, and progress-summary rendering.
Onboarding & Development
sidecars/paperclip/server/src/onboarding-assets/ceo/HEARTBEAT.md, sidecars/paperclip/skills/paperclip-dev/SKILL.md, sidecars/paperclip/scripts/screenshot-pap2373.mjs, sidecars/paperclip/scripts/screenshot-subissues.mjs
Updates CEO heartbeat checklist to reference $AGENT_HOME paths; introduces comprehensive paperclip-dev skill documentation for dev operations and workflows; adds Playwright screenshot scripts for Storybook stories.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

Poem

🐰 Hop, hop—workflows bloom!
Stalled reviews caught in gloom,
Auto-recover the way,
Checklist steps guide the day.
Port rewrites, cleaner still,
Safety guards and hosted will!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync/paperclip-upstream-2026-04-28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
sidecars/paperclip/ui/src/context/CompanyContext.tsx (1)

19-33: ⚠️ Potential issue | 🟠 Major

Unauthorized company-list failures are not surfaced to consumers.

The 401 path is converted into { unauthorized: true }, but context consumers only receive error, which stays null for that branch. This makes unauthorized vs truly empty company state indistinguishable at the context boundary.

Proposed fix
 interface CompanyContextValue {
   companies: Company[];
   selectedCompanyId: string | null;
   selectedCompany: Company | null;
   selectionSource: CompanySelectionSource;
   loading: boolean;
   error: Error | null;
+  unauthorized: boolean;
   setSelectedCompanyId: (companyId: string, options?: CompanySelectionOptions) => void;
@@
   const value = useMemo(
     () => ({
       companies,
       selectedCompanyId,
       selectedCompany,
       selectionSource,
       loading: isLoading,
       error: error as Error | null,
+      unauthorized: companyListUnauthorized,
       setSelectedCompanyId,
       reloadCompanies,
       createCompany,
     }),
@@
       selectionSource,
       isLoading,
       error,
+      companyListUnauthorized,
       setSelectedCompanyId,
As per coding guidelines `sidecars/paperclip/ui/src/**/*.{ts,tsx}`: “Surface failures clearly; do not silently ignore API errors.”

Also applies to: 72-88, 157-179

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/context/CompanyContext.tsx` around lines 19 - 33,
The CompanyContext currently maps a 401 into a private object and leaves
CompanyContextValue.error null, so consumers can't distinguish "unauthorized"
from "no companies"; update the contract and loader to surface authorization
failures: add an explicit unauthorized: boolean (or set error to a meaningful
Error) to CompanyContextValue and in the company-loading logic (the function
that implements reloadCompanies / initial load and the path that currently
converts 401 into { unauthorized: true }) set context.loading=false and either
set error=new Error('Unauthorized') or unauthorized=true when a 401 is returned,
ensuring the setSelectedCompanyId/reloadCompanies flows propagate that state to
consumers instead of swallowing it.
sidecars/paperclip/server/src/services/heartbeat.ts (1)

3799-3818: ⚠️ Potential issue | 🟠 Major

Make the stale-issue check atomic with the claim.

evaluateQueuedRunStaleness() runs before the status = 'running' update, but nothing locks the issue row or revalidates it during the claim. If the issue is reassigned/closed in that gap, this run can still start and executeRun() will proceed with stale issue context. Please fold the issue read + staleness decision + run claim into one transaction, or make the claim conditional on the same issue state you just validated.

🧹 Nitpick comments (12)
sidecars/paperclip/ui/src/components/InlineEntitySelector.test.tsx (1)

24-79: Guarantee listener/root cleanup even on assertion failures.

At Line 75–78 cleanup only runs on the happy path. If any assertion fails earlier, the document listener and mounted root can leak into later tests.

Proposed hardening
 it("keeps handled search navigation keys inside the popover", async () => {
   const root = createRoot(container);
   const onChange = vi.fn();
   const documentKeyDown = vi.fn();
   document.addEventListener("keydown", documentKeyDown);
-
-  act(() => {
-    root.render(
-      <InlineEntitySelector
-        value=""
-        options={[
-          { id: "agent:agent-1", label: "CodexCoder" },
-          { id: "agent:agent-2", label: "DesignBot" },
-        ]}
-        placeholder="Assignee"
-        noneLabel="No assignee"
-        searchPlaceholder="Search assignees..."
-        emptyMessage="No assignees found."
-        onChange={onChange}
-      />,
-    );
-  });
-
-  const trigger = container.querySelector("button") as HTMLButtonElement | null;
-  expect(trigger).not.toBeNull();
-
-  await act(async () => {
-    trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
-  });
-
-  const searchInput = document.querySelector('input[placeholder="Search assignees..."]') as HTMLInputElement | null;
-  expect(searchInput).not.toBeNull();
-  searchInput?.focus();
-
-  await act(async () => {
-    await Promise.resolve();
-  });
-
-  await act(async () => {
-    searchInput?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "ArrowDown" }));
-  });
-
-  expect(documentKeyDown).not.toHaveBeenCalled();
-
-  await act(async () => {
-    searchInput?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
-  });
-
-  expect(documentKeyDown).not.toHaveBeenCalled();
-  expect(onChange).toHaveBeenCalledWith("agent:agent-1");
-
-  document.removeEventListener("keydown", documentKeyDown);
-  act(() => {
-    root.unmount();
-  });
+  try {
+    act(() => {
+      root.render(
+        <InlineEntitySelector
+          value=""
+          options={[
+            { id: "agent:agent-1", label: "CodexCoder" },
+            { id: "agent:agent-2", label: "DesignBot" },
+          ]}
+          placeholder="Assignee"
+          noneLabel="No assignee"
+          searchPlaceholder="Search assignees..."
+          emptyMessage="No assignees found."
+          onChange={onChange}
+        />,
+      );
+    });
+
+    const trigger = container.querySelector("button") as HTMLButtonElement | null;
+    expect(trigger).not.toBeNull();
+
+    await act(async () => {
+      trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+    });
+
+    const searchInput = document.querySelector('input[placeholder="Search assignees..."]') as HTMLInputElement | null;
+    expect(searchInput).not.toBeNull();
+    searchInput?.focus();
+
+    await act(async () => {
+      await Promise.resolve();
+    });
+
+    await act(async () => {
+      searchInput?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "ArrowDown" }));
+    });
+
+    expect(documentKeyDown).not.toHaveBeenCalled();
+
+    await act(async () => {
+      searchInput?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
+    });
+
+    expect(documentKeyDown).not.toHaveBeenCalled();
+    expect(onChange).toHaveBeenCalledWith("agent:agent-1");
+  } finally {
+    document.removeEventListener("keydown", documentKeyDown);
+    act(() => {
+      root.unmount();
+    });
+  }
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/components/InlineEntitySelector.test.tsx` around
lines 24 - 79, The test "keeps handled search navigation keys inside the
popover" can leak the documentKeyDown listener and the mounted root when
assertions fail; wrap the body that sets up
document.addEventListener(documentKeyDown) and root (created via createRoot) in
a try/finally and perform cleanup in the finally block: always call
document.removeEventListener("keydown", documentKeyDown) and root.unmount()
(using the same root variable) inside finally so listener/root are removed
regardless of test failures; keep existing use of act() around mount/unmount and
retain onChange assertions inside the try.
sidecars/paperclip/releases/v2026.427.0.md (1)

84-84: Consider adding a concrete install command in the upgrade step.

This line is clear conceptually; adding a copy-paste command (for example with npm/pnpm) would reduce upgrade friction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/releases/v2026.427.0.md` at line 84, Update the upgrade
note that currently says "install `@paperclipai/plugin-e2b`" to include
copy-paste install commands for common package managers (e.g., npm and pnpm) so
users can run them directly; edit the sentence referencing installing
`@paperclipai/plugin-e2b` and the phrase "to keep using sandboxed runtimes after
upgrading" to append explicit npm/pnpm install examples and an optional
devDependency flag for clarity.
sidecars/paperclip/scripts/ensure-plugin-build-deps.mjs (1)

36-38: Non-portable sleep implementation using Atomics.wait.

Atomics.wait with SharedArrayBuffer requires specific Node.js flags (--experimental-shared-memory in older versions) and may fail in environments where SharedArrayBuffer is restricted due to cross-origin isolation requirements (e.g., some CI runners or sandboxed contexts).

A simpler synchronous delay using setTimeout would be more portable:

♻️ Suggested more portable sleep implementation
 function sleep(ms) {
-  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
+  const end = Date.now() + ms;
+  while (Date.now() < end) {
+    // Busy-wait; acceptable for short polling intervals in build scripts
+  }
 }

Alternatively, if blocking is acceptable, use child_process.spawnSync with a shell sleep command, or refactor to async with await new Promise(r => setTimeout(r, ms)).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/scripts/ensure-plugin-build-deps.mjs` around lines 36 -
38, Replace the non-portable synchronous sleep implementation that uses
Atomics.wait in the sleep function with a portable alternative: either make
sleep async and return a Promise that resolves via setTimeout (e.g., change
function sleep(ms) to async sleep(ms) { await new Promise(r => setTimeout(r,
ms)); }) or, if blocking behavior is required, use a platform call like
child_process.spawnSync('sleep', [seconds]) inside function sleep; update all
callers of sleep accordingly; key symbol to change is the sleep function
currently using Atomics.wait.
sidecars/paperclip/ui/src/context/CompanyContext.test.tsx (1)

127-194: Add a regression test for non-401 company list failures.

Please add a provider test where companiesApi.list rejects with a non-401 error and assert the stored selection is preserved; this closes an important error-path gap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/context/CompanyContext.test.tsx` around lines 127 -
194, Add a new test in CompanyProvider that simulates companiesApi.list
rejecting with a non-401 error and asserts the stored selection is preserved:
set localStorage.setItem("paperclip.selectedCompanyId", "stale-company"),
mockCompaniesApi.list to return a rejected promise (non-401), render the
provider inside QueryClientProvider with the existing QueryClient and Probe
(onSelectedCompanyId collects values), then assert seen remains
["stale-company"] (or initial emission behavior consistent with other tests) and
localStorage.getItem("paperclip.selectedCompanyId") is still "stale-company";
reference CompanyProvider, mockCompaniesApi.list, Probe, queryClient and
queryKeys.companies.all to locate and mirror the existing test structure and
cleanup.
sidecars/paperclip/packages/adapter-utils/src/server-utils.ts (1)

1075-1084: Code is currently functional but lacks defensive handling for YAML variants.

Only one SKILL.md file in the codebase uses the required field (paperclip-dev/SKILL.md with required: false), and it matches the current regex exactly. However, the pattern ^\s*required\s*:\s*false\s*$ would silently fail to recognize valid YAML variants: case changes (False/FALSE), quoted values ("false"/'false'), or inline comments (false # optional). To prevent future issues, consider hardening the pattern to handle these scenarios:

-    return !/^\s*required\s*:\s*false\s*$/m.test(frontmatter);
+    return !/^\s*required\s*:\s*(?:false|False|FALSE|["']false["'])\s*(?:#.*)?$/m.test(frontmatter);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/packages/adapter-utils/src/server-utils.ts` around lines
1075 - 1084, The readSkillRequired function's regex for detecting "required:
false" is too strict; update the test in readSkillRequired to a case-insensitive
pattern that accepts quoted values and trailing comments (e.g.
/^\s*required\s*:\s*(?:"false"|'false'|false)\b.*$/i) so "False", "'false'",
"\"false\"", or "false # comment" are treated as false; replace the existing
/^\s*required\s*:\s*false\s*$/m.test(frontmatter) with this new pattern and
run/add tests for variants.
sidecars/paperclip/server/src/routes/issues.ts (1)

952-952: Prefer reusing parseBooleanQuery for includeBlockedBy parsing.

This keeps boolean-query parsing behavior consistent across this route file.

♻️ Proposed refactor
-      includeBlockedBy: req.query.includeBlockedBy === "true" || req.query.includeBlockedBy === "1",
+      includeBlockedBy: parseBooleanQuery(req.query.includeBlockedBy),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/server/src/routes/issues.ts` at line 952, Replace the
manual boolean parsing for includeBlockedBy with the shared helper to keep
behavior consistent: use parseBooleanQuery(req.query.includeBlockedBy) (same
helper used elsewhere in this route file) when setting includeBlockedBy instead
of the current expression that checks "true" or "1"; update the assignment where
includeBlockedBy is computed so it references parseBooleanQuery and leaves the
rest of the parameter handling unchanged.
sidecars/paperclip/ui/src/components/StatusIcon.tsx (1)

43-49: Add a defensive fallback for stalled blocker count text.

If stalledBlockerCount is absent in a partial/older payload, the current branch can render "Blocked · undefined reviews..." in title/aria text. A small fallback keeps messaging stable.

Suggested tweak
   if (blockerAttention.reason === "stalled_review") {
-    const count = blockerAttention.stalledBlockerCount;
+    const count = blockerAttention.stalledBlockerCount ?? blockerAttention.coveredBlockerCount ?? 1;
     const leaf = blockerAttention.sampleStalledBlockerIdentifier ?? blockerAttention.sampleBlockerIdentifier;
     if (count === 1 && leaf) return `Blocked · review stalled on ${leaf}`;
     if (count === 1) return "Blocked · review stalled with no clear next step";
     return `Blocked · ${count} reviews stalled with no clear next step`;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/components/StatusIcon.tsx` around lines 43 - 49,
The branch handling blockerAttention.reason === "stalled_review" can render
"undefined" when stalledBlockerCount is missing; update the logic in
StatusIcon.tsx by defaulting stalledBlockerCount (e.g., const count =
blockerAttention.stalledBlockerCount ?? 0) and add a case for count === 0 (or
treat 0 as the generic stalled message) so the returned strings using count
never interpolate "undefined" (references: blockerAttention,
stalledBlockerCount, sampleStalledBlockerIdentifier, sampleBlockerIdentifier).
sidecars/paperclip/ui/src/api/instanceSettings.ts (1)

22-37: Consider extracting the inline return type to shared types.

The runIssueGraphLivenessAutoRecovery return type is defined inline with 11 fields. For consistency with IssueGraphLivenessAutoRecoveryPreview (which is imported from shared), consider defining a corresponding IssueGraphLivenessAutoRecoveryRunResult type in the shared package.

This improves maintainability if the server response shape changes, and enables type reuse in tests or other consumers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/api/instanceSettings.ts` around lines 22 - 37,
Extract the inline return type used by runIssueGraphLivenessAutoRecovery into a
shared type (e.g. IssueGraphLivenessAutoRecoveryRunResult) in the shared
package, export it, then import that type into
sidecars/paperclip/ui/src/api/instanceSettings.ts and replace the current inline
object type in the api.post<> call with IssueGraphLivenessAutoRecoveryRunResult;
update any related tests or consumers to use the new shared type to keep shapes
consistent with IssueGraphLivenessAutoRecoveryPreview.
sidecars/paperclip/docker/ecs-task-definition.json (2)

12-12: Avoid using :latest tag in production task definitions.

The :latest tag makes deployments non-deterministic and complicates rollbacks. Consider using immutable tags (e.g., Git SHA, semantic version, or build timestamp) so each deployment references a specific, reproducible image.

💡 Suggested approach
-      "image": "<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/paperclip-server:latest",
+      "image": "<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/paperclip-server:<IMAGE_TAG>",

Then update the templating in the deployment guide to substitute <IMAGE_TAG> with a specific version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/docker/ecs-task-definition.json` at line 12, The task
definition's container image uses the non-deterministic ":latest" tag in the
"image" field; change it to reference an immutable tag (for example replace
":latest" with a templated placeholder like "<IMAGE_TAG>" or a specific Git
SHA/semver) so each deployment points to a reproducible image, and update the
deployment templating/docs to ensure the chosen IMAGE_TAG is supplied during
CI/CD releases (look for the "image" entry in ecs-task-definition.json to
modify).

31-31: Consider the risks of auto-applying migrations in production.

PAPERCLIP_MIGRATION_AUTO_APPLY=true runs database migrations automatically on startup. For production deployments, this can cause issues if migrations are long-running (blocking health checks) or destructive (no easy rollback). Consider documenting this trade-off or offering a safer default with a manual migration step.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/docker/ecs-task-definition.json` at line 31, The task
definition sets PAPERCLIP_MIGRATION_AUTO_APPLY=true which auto-runs DB
migrations on startup; change the default to "false" (or remove the env var) and
document the migration process so production requires a separate, manual
migration step; update any deploy/run scripts or CI to run a dedicated migration
job or one-off ECS task that invokes the migrate command, and add
guidance/comments around the PAPERCLIP_MIGRATION_AUTO_APPLY env var and safe
rollback/timeout expectations so operators know how to enable auto-apply
intentionally.
sidecars/paperclip/docs/deploy/aws-ecs.md (2)

187-189: Consider adding a wait loop for mount target availability.

Mount target creation is asynchronous. While subsequent steps (ECS service creation) happen much later, adding a brief polling loop would make the guide more robust for automation.

💡 Suggested addition
# Wait for mount targets to become available
echo "Waiting for mount targets..."
while true; do
  STATUS=$(aws efs describe-mount-targets --file-system-id $EFS_ID \
    --query 'MountTargets[*].LifeCycleState' --output text)
  if echo "$STATUS" | grep -qv "creating"; then
    break
  fi
  sleep 10
done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/docs/deploy/aws-ecs.md` around lines 187 - 189, Add a
brief polling loop after the existing aws efs describe-mount-targets call to
wait until the EFS mount targets finish creating: repeatedly call aws efs
describe-mount-targets for the $EFS_ID and check the
MountTargets[*].LifeCycleState until none are "creating", sleeping (e.g., 10s)
between retries and echoing progress; reference the existing use of $EFS_ID and
the describe-mount-targets command to locate where to insert the loop.

140-155: Consider noting production scaling recommendations.

The guide uses db.t4g.micro and single-AZ for cost efficiency, which is appropriate for getting started. For production workloads with higher availability requirements, consider adding a brief note recommending Multi-AZ and larger instance classes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/docs/deploy/aws-ecs.md` around lines 140 - 155, Add a
short production note next to the RDS creation example that highlights the
current example uses db.t4g.micro and --no-multi-az for cost/testing, and
recommend for production switching to a larger --db-instance-class (e.g., r5/
m6g family) and enabling Multi-AZ (remove --no-multi-az) and configuring
automated backups/replicas for high availability and durability; reference the
aws rds create-db-instance snippet, db.t4g.micro, and the --no-multi-az flag so
readers know exactly which parts to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@sidecars/paperclip/docs/guides/board-operator/delegation.md`:
- Line 50: Update the lifecycle steps that currently read as always-required to
make hire approvals conditional: find the lifecycle occurrences that mention
hiring or approval flows (the step that now says "Hires new agents" and the
other lifecycle steps that reference hiring/approval earlier and later in the
document) and append or replace wording to "when hire approvals are enabled" (or
equivalent phrasing) so the lifecycle consistently indicates approvals are
optional per company settings; ensure all mentions (the initial lifecycle step,
the earlier lifecycle description, and the later approval step) use the same
conditional language.

In `@sidecars/paperclip/docs/start/core-concepts.md`:
- Around line 60-62: Update the Governance section's language to match the
configurable wording in the earlier paragraph: replace the absolute phrase that
hiring “must” be board-approved with a conditional statement that hiring is
subject to the project's governance settings (e.g., “Hiring may require board
approval when hire approvals are enabled” or “Hiring is subject to governance
settings and can require board approval if hire approvals are enabled”); ensure
the section references the configurable hire-approval toggle and the existing
"How Delegation Works" guide to keep policy consistent with the earlier sentence
about approvals being optional/configurable.

In `@sidecars/paperclip/packages/adapters/pi-local/src/server/execute.ts`:
- Around line 232-239: The env export call to applyPaperclipWorkspaceEnv is
using workspaceCwd instead of the actual runtime cwd, causing
PAPERCLIP_WORKSPACE_CWD to be wrong when cwd is overridden; update the call so
it passes effectiveWorkspaceCwd (the value used for execution) in place of
workspaceCwd (and ensure any agentHome override logic that computes
effectiveWorkspaceCwd is preserved) so applyPaperclipWorkspaceEnv,
applyPaperclipWorkspaceEnv(..., { workspaceCwd: effectiveWorkspaceCwd,
workspaceSource, workspaceId, workspaceRepoUrl, workspaceRepoRef, agentHome })
uses the effective path.

In `@sidecars/paperclip/packages/shared/src/constants.ts`:
- Line 45: ROLE_PERMISSIONS is missing an explicit entry for the newly
introduced "security" role, which can produce undefined permission resolution;
add a "security" key to the ROLE_PERMISSIONS object (alongside existing keys
like "admin", "member", "provision") and populate it with the intended
permission array (either a copy of an existing role's permissions or the exact
set required for security users) so lookups for the "security" role return a
defined permissions array.

In `@sidecars/paperclip/releases/v2026.427.0.md`:
- Around line 66-83: The release note's blanket claim "no existing data is
modified or removed" is inaccurate; update the note to either drop that
guarantee or explicitly describe non-additive actions in migrations 0057, 0060,
0062, and 0068: state that 0057 runs UPDATEs to reject duplicate pending join
requests, 0060 DELETEs duplicate rows from issue_reference_mentions (keeping the
earliest per key), 0062 drops and recreates the issues_open_routine_execution_uq
index with a new definition, and 0068 drops and recreates the
environments_company_driver_idx index (and thus may change indexes); keep the
rest of the migrations described as additive.

In `@sidecars/paperclip/server/src/services/company-portability.ts`:
- Around line 2266-2268: The change makes lack of
requireBoardApprovalForNewAgents ambiguous because readCompanyApprovalDefault
currently returns false; update portability handling to preserve compatibility
by detecting bundle version or always serializing the field: modify
readCompanyApprovalDefault to accept/inspect the bundle's portability version
(or metadata) and return true for older versions while returning the new default
for newer versions, or change the exporter/importer code paths (the
serialization logic that omits requireBoardApprovalForNewAgents and the
deserialization in readCompanyApprovalDefault) to always emit and read this
field explicitly until you bump the portability format; locate and update the
functions named readCompanyApprovalDefault and the corresponding
serializer/deserializer paths referenced in the other spots (the similar blocks
around the other noted occurrences) to implement version branching or guaranteed
serialization.

In `@sidecars/paperclip/server/src/services/heartbeat.ts`:
- Around line 3941-3993: In evaluateQueuedRunStaleness, the terminal-status
branch currently treats only resumeIntent/followUpRequested and wakeCommentId as
resume signals; change the check so explicit resume fields set by enqueueWakeup
(context.resumeFromRunId, context.resumeSessionDisplayId,
context.resumeSessionParams) are also considered as resume intent — i.e.,
compute a combined resume flag (or extend the existing resumeIntent) to include
those three context properties before the if (!resumeIntent && !wakeCommentId)
return, so manual/explicit resumes against terminal issues are not marked stale;
reference evaluateQueuedRunStaleness, context.resumeIntent,
context.followUpRequested, deriveCommentId, and the explicit resume fields
resumeFromRunId/resumeSessionDisplayId/resumeSessionParams.

In `@sidecars/paperclip/server/src/services/issues.ts`:
- Around line 1421-1475: blockedByMapForIssues currently returns only direct
blockers so the UI loses "terminalBlockers" context; update it to populate
terminalBlockers by reusing the existing enrichment logic (e.g. call
getIssueRelationSummaryMap or the same helper that computes terminalBlockers)
instead of returning raw direct relations: fetch the related issues as you do
now, then call the terminal-blocker enrichment routine for the set of relatedIds
(or replace the loop with the getIssueRelationSummaryMap flow) and attach the
resulting terminalBlockers to each IssueRelationIssueSummary before sorting and
returning the Map so includeBlockedBy responses include the full "Ultimately
waiting on"/stalled-leaf info.
- Around line 1069-1083: The current query only checks issues.originId against
chunk so recovery children (which are parented under the stalled blocker) are
missed; update the query to also select and filter on issues.parentId (add
parentId to the select and add OR inArray(issues.parentId, chunk) to the where
clause) and then, when iterating recoveryRows, if row.originId add that to
explicitWaitingIssueIds else if row.parentId add row.parentId to
explicitWaitingIssueIds (refer to recoveryRows, explicitWaitingIssueIds,
issues.originId, issues.parentId, and chunk).

In `@sidecars/paperclip/server/src/services/recovery/service.ts`:
- Around line 1839-1846: The innerJoin between issueApprovals and approvals
currently matches only by approvalId which allows cross-company matches; update
the join predicate used in the innerJoin call (the call that references
issueApprovals and approvals, e.g., innerJoin(approvals,
eq(issueApprovals.approvalId, approvals.id))) to also require company scoping by
AND-ing a companyId equality (eq(issueApprovals.companyId, approvals.companyId))
so the join enforces same company; ensure the combined predicate preserves the
existing status filter on approvals.

In `@sidecars/paperclip/skills/paperclip-dev/SKILL.md`:
- Around line 15-17: The MD028 lint error is caused by a blank line separating
two consecutive blockquote paragraphs; in SKILL.md merge the blockquote into one
continuous block or add a `>` on the empty separator line so the two quoted
paragraphs remain part of the same blockquote. Locate the block starting with
the quoted lines containing "OPEN SOURCE HYGIENE:" and "MANDATORY:" and either
remove the blank line between them or prefix that blank line with `>` to satisfy
the markdownlint rule.
- Around line 42-43: Replace the hardcoded "master" in the git sync commands
with dynamic default-branch detection: compute the remote default branch (e.g.
branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed
's|^origin/||' || git symbolic-ref --short HEAD')) and then use git fetch origin
&& git pull origin "$branch" in place of git fetch origin && git pull origin
master; apply the same change to the other occurrence (lines around the second
git command block).

In `@sidecars/paperclip/ui/src/components/IssuesList.tsx`:
- Around line 848-853: The checklistAffordanceEnabled computation incorrectly
checks defaultSortField instead of the active sort; change the condition to use
the current/active sort field (e.g., viewState.sortField) rather than
defaultSortField so the checklist UI appears only when the user-selected sort is
"workflow" and viewState.groupBy === "none". Update the useMemo to reference
viewState.sortField (or the actual active sort property on viewState) in place
of defaultSortField and keep the same dependency array.

In `@sidecars/paperclip/ui/src/context/CompanyContext.tsx`:
- Around line 72-116: The effect that auto-selects/clears the stored company
runs even when the query errored (non-401), causing localStorage to be wiped on
transient failures; change the useEffect to bail out early when the query has an
error (the error from useQuery) so it only clears selection when we actually
have successful data/authorized empty list—i.e. add a guard like if (error)
return; at the top of the effect (before checking isLoading/companies) so
shouldClearStoredCompanySelection, resolveBootstrapCompanySelection,
localStorage.removeItem(STORAGE_KEY), setSelectedCompanyIdState and
setSelectionSource only run when there is no query error.

In `@sidecars/paperclip/ui/src/pages/AgentDetail.tsx`:
- Line 15: The hosted-mode early return in AgentDetail.tsx causes conditional
hook execution (useHostedMode, useParams, useCompany, etc.); fix it by
extracting the guard into an outer wrapper component (e.g., HostedGuard or
AgentDetailWrapper) that calls useHostedMode and conditionally renders an inner
component (e.g., AgentDetailInner) — move all existing hooks and the current
component body into that inner component so useParams, useCompany and all other
hooks run unconditionally, and have the outer wrapper solely decide whether to
render the inner component based on the hosted check.

In `@sidecars/paperclip/ui/src/pages/InstanceExperimentalSettings.tsx`:
- Around line 123-133: The early hosted-mode return in
InstanceExperimentalSettings is causing conditional hook execution; refactor by
creating a tiny wrapper that calls useHostedMode and returns <Navigate to="/"
replace /> when isHosted is true, and move all existing hook usage and JSX into
a new child component (e.g., InstanceExperimentalSettingsInner) so that
useBreadcrumbs, useQueryClient, useState hooks (lookbackHoursDraft,
previewDialogOpen, pendingPreview, actionError), and any
useEffect/useQuery/useMutation calls are always invoked unconditionally inside
that inner component; keep the exported InstanceExperimentalSettings as the
wrapper that only checks isHosted and renders InstanceExperimentalSettingsInner
when not hosted.

In `@sidecars/paperclip/ui/storybook/stories/sub-issues-workflow.stories.tsx`:
- Around line 195-198: The story seeds only a partial
InstanceExperimentalSettings object causing missing flags to be undefined;
update the fixture used in
client.setQueryData(queryKeys.instance.experimentalSettings, ...) to include the
full shape of InstanceExperimentalSettings (add all newly required
boolean/fields introduced in the PR) so consumers read real values; locate the
seed in sub-issues-workflow.stories.tsx and populate every required property on
InstanceExperimentalSettings (matching types) instead of just
enableIsolatedWorkspaces and enableRoutineTriggers.

---

Outside diff comments:
In `@sidecars/paperclip/ui/src/context/CompanyContext.tsx`:
- Around line 19-33: The CompanyContext currently maps a 401 into a private
object and leaves CompanyContextValue.error null, so consumers can't distinguish
"unauthorized" from "no companies"; update the contract and loader to surface
authorization failures: add an explicit unauthorized: boolean (or set error to a
meaningful Error) to CompanyContextValue and in the company-loading logic (the
function that implements reloadCompanies / initial load and the path that
currently converts 401 into { unauthorized: true }) set context.loading=false
and either set error=new Error('Unauthorized') or unauthorized=true when a 401
is returned, ensuring the setSelectedCompanyId/reloadCompanies flows propagate
that state to consumers instead of swallowing it.

---

Nitpick comments:
In `@sidecars/paperclip/docker/ecs-task-definition.json`:
- Line 12: The task definition's container image uses the non-deterministic
":latest" tag in the "image" field; change it to reference an immutable tag (for
example replace ":latest" with a templated placeholder like "<IMAGE_TAG>" or a
specific Git SHA/semver) so each deployment points to a reproducible image, and
update the deployment templating/docs to ensure the chosen IMAGE_TAG is supplied
during CI/CD releases (look for the "image" entry in ecs-task-definition.json to
modify).
- Line 31: The task definition sets PAPERCLIP_MIGRATION_AUTO_APPLY=true which
auto-runs DB migrations on startup; change the default to "false" (or remove the
env var) and document the migration process so production requires a separate,
manual migration step; update any deploy/run scripts or CI to run a dedicated
migration job or one-off ECS task that invokes the migrate command, and add
guidance/comments around the PAPERCLIP_MIGRATION_AUTO_APPLY env var and safe
rollback/timeout expectations so operators know how to enable auto-apply
intentionally.

In `@sidecars/paperclip/docs/deploy/aws-ecs.md`:
- Around line 187-189: Add a brief polling loop after the existing aws efs
describe-mount-targets call to wait until the EFS mount targets finish creating:
repeatedly call aws efs describe-mount-targets for the $EFS_ID and check the
MountTargets[*].LifeCycleState until none are "creating", sleeping (e.g., 10s)
between retries and echoing progress; reference the existing use of $EFS_ID and
the describe-mount-targets command to locate where to insert the loop.
- Around line 140-155: Add a short production note next to the RDS creation
example that highlights the current example uses db.t4g.micro and --no-multi-az
for cost/testing, and recommend for production switching to a larger
--db-instance-class (e.g., r5/ m6g family) and enabling Multi-AZ (remove
--no-multi-az) and configuring automated backups/replicas for high availability
and durability; reference the aws rds create-db-instance snippet, db.t4g.micro,
and the --no-multi-az flag so readers know exactly which parts to change.

In `@sidecars/paperclip/packages/adapter-utils/src/server-utils.ts`:
- Around line 1075-1084: The readSkillRequired function's regex for detecting
"required: false" is too strict; update the test in readSkillRequired to a
case-insensitive pattern that accepts quoted values and trailing comments (e.g.
/^\s*required\s*:\s*(?:"false"|'false'|false)\b.*$/i) so "False", "'false'",
"\"false\"", or "false # comment" are treated as false; replace the existing
/^\s*required\s*:\s*false\s*$/m.test(frontmatter) with this new pattern and
run/add tests for variants.

In `@sidecars/paperclip/releases/v2026.427.0.md`:
- Line 84: Update the upgrade note that currently says "install
`@paperclipai/plugin-e2b`" to include copy-paste install commands for common
package managers (e.g., npm and pnpm) so users can run them directly; edit the
sentence referencing installing `@paperclipai/plugin-e2b` and the phrase "to
keep using sandboxed runtimes after upgrading" to append explicit npm/pnpm
install examples and an optional devDependency flag for clarity.

In `@sidecars/paperclip/scripts/ensure-plugin-build-deps.mjs`:
- Around line 36-38: Replace the non-portable synchronous sleep implementation
that uses Atomics.wait in the sleep function with a portable alternative: either
make sleep async and return a Promise that resolves via setTimeout (e.g., change
function sleep(ms) to async sleep(ms) { await new Promise(r => setTimeout(r,
ms)); }) or, if blocking behavior is required, use a platform call like
child_process.spawnSync('sleep', [seconds]) inside function sleep; update all
callers of sleep accordingly; key symbol to change is the sleep function
currently using Atomics.wait.

In `@sidecars/paperclip/server/src/routes/issues.ts`:
- Line 952: Replace the manual boolean parsing for includeBlockedBy with the
shared helper to keep behavior consistent: use
parseBooleanQuery(req.query.includeBlockedBy) (same helper used elsewhere in
this route file) when setting includeBlockedBy instead of the current expression
that checks "true" or "1"; update the assignment where includeBlockedBy is
computed so it references parseBooleanQuery and leaves the rest of the parameter
handling unchanged.

In `@sidecars/paperclip/ui/src/api/instanceSettings.ts`:
- Around line 22-37: Extract the inline return type used by
runIssueGraphLivenessAutoRecovery into a shared type (e.g.
IssueGraphLivenessAutoRecoveryRunResult) in the shared package, export it, then
import that type into sidecars/paperclip/ui/src/api/instanceSettings.ts and
replace the current inline object type in the api.post<> call with
IssueGraphLivenessAutoRecoveryRunResult; update any related tests or consumers
to use the new shared type to keep shapes consistent with
IssueGraphLivenessAutoRecoveryPreview.

In `@sidecars/paperclip/ui/src/components/InlineEntitySelector.test.tsx`:
- Around line 24-79: The test "keeps handled search navigation keys inside the
popover" can leak the documentKeyDown listener and the mounted root when
assertions fail; wrap the body that sets up
document.addEventListener(documentKeyDown) and root (created via createRoot) in
a try/finally and perform cleanup in the finally block: always call
document.removeEventListener("keydown", documentKeyDown) and root.unmount()
(using the same root variable) inside finally so listener/root are removed
regardless of test failures; keep existing use of act() around mount/unmount and
retain onChange assertions inside the try.

In `@sidecars/paperclip/ui/src/components/StatusIcon.tsx`:
- Around line 43-49: The branch handling blockerAttention.reason ===
"stalled_review" can render "undefined" when stalledBlockerCount is missing;
update the logic in StatusIcon.tsx by defaulting stalledBlockerCount (e.g.,
const count = blockerAttention.stalledBlockerCount ?? 0) and add a case for
count === 0 (or treat 0 as the generic stalled message) so the returned strings
using count never interpolate "undefined" (references: blockerAttention,
stalledBlockerCount, sampleStalledBlockerIdentifier, sampleBlockerIdentifier).

In `@sidecars/paperclip/ui/src/context/CompanyContext.test.tsx`:
- Around line 127-194: Add a new test in CompanyProvider that simulates
companiesApi.list rejecting with a non-401 error and asserts the stored
selection is preserved: set localStorage.setItem("paperclip.selectedCompanyId",
"stale-company"), mockCompaniesApi.list to return a rejected promise (non-401),
render the provider inside QueryClientProvider with the existing QueryClient and
Probe (onSelectedCompanyId collects values), then assert seen remains
["stale-company"] (or initial emission behavior consistent with other tests) and
localStorage.getItem("paperclip.selectedCompanyId") is still "stale-company";
reference CompanyProvider, mockCompaniesApi.list, Probe, queryClient and
queryKeys.companies.all to locate and mirror the existing test structure and
cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8067544d-6f87-4e6f-8cf4-3f928ce3c5af

📥 Commits

Reviewing files that changed from the base of the PR and between 5ccf4ad and 366a5c1.

⛔ Files ignored due to path filters (7)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • sidecars/paperclip/doc/assets/pap-2189/desktop-1440x900-dark.png is excluded by !**/*.png
  • sidecars/paperclip/doc/assets/pap-2189/desktop-1440x900-light.png is excluded by !**/*.png
  • sidecars/paperclip/doc/assets/pap-2189/mobile-390x844-dark.png is excluded by !**/*.png
  • sidecars/paperclip/doc/assets/pap-2189/mobile-390x844-light.png is excluded by !**/*.png
  • sidecars/paperclip/docs/pr-screenshots/pr-4616/sidebar-agent-actions.png is excluded by !**/*.png
  • sidecars/paperclip/docs/pr-screenshots/pr-4616/sidebar-agent-row.png is excluded by !**/*.png
📒 Files selected for processing (123)
  • sidecars/paperclip/.upstream-baseline
  • sidecars/paperclip/README.md
  • sidecars/paperclip/cli/README.md
  • sidecars/paperclip/cli/src/__tests__/company-import-export-e2e.test.ts
  • sidecars/paperclip/cli/src/__tests__/worktree.test.ts
  • sidecars/paperclip/cli/src/commands/worktree-lib.ts
  • sidecars/paperclip/doc/SPEC-implementation.md
  • sidecars/paperclip/doc/execution-semantics.md
  • sidecars/paperclip/docker/ecs-task-definition.json
  • sidecars/paperclip/docs/deploy/aws-ecs.md
  • sidecars/paperclip/docs/deploy/overview.md
  • sidecars/paperclip/docs/guides/board-operator/delegation.md
  • sidecars/paperclip/docs/start/core-concepts.md
  • sidecars/paperclip/packages/adapter-utils/src/server-utils.test.ts
  • sidecars/paperclip/packages/adapter-utils/src/server-utils.ts
  • sidecars/paperclip/packages/adapters/claude-local/src/server/execute.ts
  • sidecars/paperclip/packages/adapters/codex-local/src/server/execute.ts
  • sidecars/paperclip/packages/adapters/cursor-local/src/server/execute.ts
  • sidecars/paperclip/packages/adapters/gemini-local/src/server/execute.ts
  • sidecars/paperclip/packages/adapters/opencode-local/src/server/execute.ts
  • sidecars/paperclip/packages/adapters/pi-local/src/server/execute.ts
  • sidecars/paperclip/packages/db/src/migrations/0071_default_hire_approval_off.sql
  • sidecars/paperclip/packages/db/src/migrations/0072_large_sandman.sql
  • sidecars/paperclip/packages/db/src/migrations/meta/0072_snapshot.json
  • sidecars/paperclip/packages/db/src/migrations/meta/_journal.json
  • sidecars/paperclip/packages/db/src/schema/companies.ts
  • sidecars/paperclip/packages/db/src/schema/issues.ts
  • sidecars/paperclip/packages/plugins/examples/plugin-authoring-smoke-example/package.json
  • sidecars/paperclip/packages/plugins/examples/plugin-file-browser-example/package.json
  • sidecars/paperclip/packages/plugins/examples/plugin-hello-world-example/package.json
  • sidecars/paperclip/packages/plugins/examples/plugin-kitchen-sink-example/package.json
  • sidecars/paperclip/packages/plugins/examples/plugin-orchestration-smoke-example/package.json
  • sidecars/paperclip/packages/plugins/paperclip-plugin-fake-sandbox/package.json
  • sidecars/paperclip/packages/plugins/sandbox-providers/e2b/package.json
  • sidecars/paperclip/packages/plugins/sdk/package.json
  • sidecars/paperclip/packages/shared/src/adapter-types.test.ts
  • sidecars/paperclip/packages/shared/src/constants.ts
  • sidecars/paperclip/packages/shared/src/index.ts
  • sidecars/paperclip/packages/shared/src/types/index.ts
  • sidecars/paperclip/packages/shared/src/types/instance.ts
  • sidecars/paperclip/packages/shared/src/types/issue.ts
  • sidecars/paperclip/packages/shared/src/validators/index.ts
  • sidecars/paperclip/packages/shared/src/validators/instance.ts
  • sidecars/paperclip/releases/v2026.427.0.md
  • sidecars/paperclip/scripts/ensure-plugin-build-deps.mjs
  • sidecars/paperclip/scripts/screenshot-pap2373.mjs
  • sidecars/paperclip/scripts/screenshot-subissues.mjs
  • sidecars/paperclip/server/package.json
  • sidecars/paperclip/server/src/__tests__/agent-permissions-routes.test.ts
  • sidecars/paperclip/server/src/__tests__/agent-skills-routes.test.ts
  • sidecars/paperclip/server/src/__tests__/better-auth.test.ts
  • sidecars/paperclip/server/src/__tests__/company-portability.test.ts
  • sidecars/paperclip/server/src/__tests__/company-skills-service.test.ts
  • sidecars/paperclip/server/src/__tests__/heartbeat-dependency-scheduling.test.ts
  • sidecars/paperclip/server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts
  • sidecars/paperclip/server/src/__tests__/heartbeat-process-recovery.test.ts
  • sidecars/paperclip/server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts
  • sidecars/paperclip/server/src/__tests__/instance-settings-routes.test.ts
  • sidecars/paperclip/server/src/__tests__/issue-blocker-attention.test.ts
  • sidecars/paperclip/server/src/__tests__/issue-liveness.test.ts
  • sidecars/paperclip/server/src/__tests__/issues-service.test.ts
  • sidecars/paperclip/server/src/__tests__/paperclip-skill-utils.test.ts
  • sidecars/paperclip/server/src/__tests__/recovery-classifiers.test.ts
  • sidecars/paperclip/server/src/__tests__/routines-routes.test.ts
  • sidecars/paperclip/server/src/__tests__/routines-service.test.ts
  • sidecars/paperclip/server/src/__tests__/server-startup-feedback-export.test.ts
  • sidecars/paperclip/server/src/__tests__/worktree-config.test.ts
  • sidecars/paperclip/server/src/auth/better-auth.ts
  • sidecars/paperclip/server/src/index.ts
  • sidecars/paperclip/server/src/onboarding-assets/ceo/HEARTBEAT.md
  • sidecars/paperclip/server/src/routes/instance-settings.ts
  • sidecars/paperclip/server/src/routes/issues.ts
  • sidecars/paperclip/server/src/routes/routines.ts
  • sidecars/paperclip/server/src/services/company-portability.ts
  • sidecars/paperclip/server/src/services/company-skills.ts
  • sidecars/paperclip/server/src/services/heartbeat.ts
  • sidecars/paperclip/server/src/services/instance-settings.ts
  • sidecars/paperclip/server/src/services/issues.ts
  • sidecars/paperclip/server/src/services/recovery/index.ts
  • sidecars/paperclip/server/src/services/recovery/issue-graph-liveness.ts
  • sidecars/paperclip/server/src/services/recovery/origins.ts
  • sidecars/paperclip/server/src/services/recovery/service.ts
  • sidecars/paperclip/server/src/services/routines.ts
  • sidecars/paperclip/server/src/worktree-config.ts
  • sidecars/paperclip/skills/paperclip-dev/SKILL.md
  • sidecars/paperclip/ui/src/api/instanceSettings.ts
  • sidecars/paperclip/ui/src/api/issues.test.ts
  • sidecars/paperclip/ui/src/api/issues.ts
  • sidecars/paperclip/ui/src/components/InlineEntitySelector.test.tsx
  • sidecars/paperclip/ui/src/components/InlineEntitySelector.tsx
  • sidecars/paperclip/ui/src/components/IssueBlockedNotice.tsx
  • sidecars/paperclip/ui/src/components/IssueChatThread.tsx
  • sidecars/paperclip/ui/src/components/IssueColumns.tsx
  • sidecars/paperclip/ui/src/components/IssueDocumentsSection.tsx
  • sidecars/paperclip/ui/src/components/IssueProperties.test.tsx
  • sidecars/paperclip/ui/src/components/IssueRow.test.tsx
  • sidecars/paperclip/ui/src/components/IssueRow.tsx
  • sidecars/paperclip/ui/src/components/IssuesList.test.tsx
  • sidecars/paperclip/ui/src/components/IssuesList.tsx
  • sidecars/paperclip/ui/src/components/MarkdownBody.test.tsx
  • sidecars/paperclip/ui/src/components/SidebarAgents.test.tsx
  • sidecars/paperclip/ui/src/components/SidebarAgents.tsx
  • sidecars/paperclip/ui/src/components/StatusIcon.test.tsx
  • sidecars/paperclip/ui/src/components/StatusIcon.tsx
  • sidecars/paperclip/ui/src/context/CompanyContext.test.tsx
  • sidecars/paperclip/ui/src/context/CompanyContext.tsx
  • sidecars/paperclip/ui/src/lib/issue-detail-subissues.test.ts
  • sidecars/paperclip/ui/src/lib/issue-detail-subissues.ts
  • sidecars/paperclip/ui/src/lib/issue-reference.test.ts
  • sidecars/paperclip/ui/src/lib/issue-reference.ts
  • sidecars/paperclip/ui/src/lib/mention-chips.ts
  • sidecars/paperclip/ui/src/lib/optimistic-issue-comments.test.ts
  • sidecars/paperclip/ui/src/lib/optimistic-issue-comments.ts
  • sidecars/paperclip/ui/src/lib/workflow-sort.test.ts
  • sidecars/paperclip/ui/src/lib/workflow-sort.ts
  • sidecars/paperclip/ui/src/pages/AgentDetail.tsx
  • sidecars/paperclip/ui/src/pages/InstanceAccess.tsx
  • sidecars/paperclip/ui/src/pages/InstanceExperimentalSettings.tsx
  • sidecars/paperclip/ui/src/pages/IssueDetail.test.tsx
  • sidecars/paperclip/ui/src/pages/IssueDetail.tsx
  • sidecars/paperclip/ui/src/pages/JoinRequestQueue.tsx
  • sidecars/paperclip/ui/storybook/stories/status-language.stories.tsx
  • sidecars/paperclip/ui/storybook/stories/sub-issues-workflow.stories.tsx
💤 Files with no reviewable changes (1)
  • sidecars/paperclip/server/src/tests/agent-permissions-routes.test.ts

- **Assigns tasks to the right agent** based on role and capabilities (e.g., engineering tasks go to the CTO or engineers, marketing tasks go to the CMO)
- **Creates subtasks** when work needs to be decomposed further
- **Hires new agents** when the team lacks capacity for a goal (subject to your approval)
- **Hires new agents** when the team lacks capacity for a goal, with hire approvals available when enabled in company settings

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make hire-approval steps conditional for consistency

Line 50 introduces the correct conditional behavior, but the lifecycle still reads as always-required (Line 34 and Line 90). Update those steps to reflect “when hire approvals are enabled” so operators don’t assume a mandatory approval queue in all setups.

Suggested doc adjustment
-3. **Approve hire requests.** When the CEO needs more capacity ...
+3. **Review hire requests (if enabled).** When hire approvals are enabled and the CEO needs more capacity ...

-3. You approve the hire
+3. If hire approvals are enabled, you approve the hire
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/docs/guides/board-operator/delegation.md` at line 50,
Update the lifecycle steps that currently read as always-required to make hire
approvals conditional: find the lifecycle occurrences that mention hiring or
approval flows (the step that now says "Hires new agents" and the other
lifecycle steps that reference hiring/approval earlier and later in the
document) and append or replace wording to "when hire approvals are enabled" (or
equivalent phrasing) so the lifecycle consistently indicates approvals are
optional per company settings; ensure all mentions (the initial lifecycle step,
the earlier lifecycle description, and the later approval step) use the same
conditional language.

Comment on lines +60 to +62
4. Hires new agents when needed, with hire approvals available when you enable them

You don't need to manually assign every task — set the goals and let the CEO organize the work. You approve key decisions (strategy, hiring) and monitor progress. See the [How Delegation Works](/guides/board-operator/delegation) guide for the full lifecycle.
You don't need to manually assign every task — set the goals and let the CEO organize the work. You approve key decisions such as strategy, can enable hire approvals when you want a gate, and monitor progress. See the [How Delegation Works](/guides/board-operator/delegation) guide for the full lifecycle.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Resolve contradiction with Governance section on hire approvals

The new wording on Lines 60–62 correctly describes approvals as optional/configurable, but Line 82 still says hiring “must” be board-approved. Please align the Governance section so the policy is consistently documented as setting-controlled.

Suggested doc adjustment
- - **Hiring agents** — agents can request to hire subordinates, but the board must approve
+ - **Hiring agents** — agents can request to hire subordinates; board approval is required only when hire approvals are enabled in company settings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/docs/start/core-concepts.md` around lines 60 - 62, Update
the Governance section's language to match the configurable wording in the
earlier paragraph: replace the absolute phrase that hiring “must” be
board-approved with a conditional statement that hiring is subject to the
project's governance settings (e.g., “Hiring may require board approval when
hire approvals are enabled” or “Hiring is subject to governance settings and can
require board approval if hire approvals are enabled”); ensure the section
references the configurable hire-approval toggle and the existing "How
Delegation Works" guide to keep policy consistent with the earlier sentence
about approvals being optional/configurable.

Comment on lines +232 to +239
applyPaperclipWorkspaceEnv(env, {
workspaceCwd,
workspaceSource,
workspaceId,
workspaceRepoUrl,
workspaceRepoRef,
agentHome,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the effective workspace cwd when exporting env.

cwd execution uses effectiveWorkspaceCwd, but the env export uses workspaceCwd. When configured cwd overrides agent_home, this can expose a mismatched PAPERCLIP_WORKSPACE_CWD.

Suggested fix
   applyPaperclipWorkspaceEnv(env, {
-    workspaceCwd,
+    workspaceCwd: effectiveWorkspaceCwd,
     workspaceSource,
     workspaceId,
     workspaceRepoUrl,
     workspaceRepoRef,
     agentHome,
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/packages/adapters/pi-local/src/server/execute.ts` around
lines 232 - 239, The env export call to applyPaperclipWorkspaceEnv is using
workspaceCwd instead of the actual runtime cwd, causing PAPERCLIP_WORKSPACE_CWD
to be wrong when cwd is overridden; update the call so it passes
effectiveWorkspaceCwd (the value used for execution) in place of workspaceCwd
(and ensure any agentHome override logic that computes effectiveWorkspaceCwd is
preserved) so applyPaperclipWorkspaceEnv, applyPaperclipWorkspaceEnv(..., {
workspaceCwd: effectiveWorkspaceCwd, workspaceSource, workspaceId,
workspaceRepoUrl, workspaceRepoRef, agentHome }) uses the effective path.

"cto",
"cmo",
"cfo",
"security",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

New security role is missing an explicit permission mapping.

Line 45/61 adds a first-class role, but ROLE_PERMISSIONS has no security key. That can lead to undefined permission resolution (or accidental empty/default behavior) for provision/member sync paths.

Proposed fix
 export const ROLE_PERMISSIONS: Record<string, readonly PermissionKey[]> = {
@@
   cfo: ["agents:create", "tasks:assign"],
+  security: ["agents:create", "tasks:assign"],
   engineer: ["agents:create", "tasks:assign"],

Also applies to: 61-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/packages/shared/src/constants.ts` at line 45,
ROLE_PERMISSIONS is missing an explicit entry for the newly introduced
"security" role, which can produce undefined permission resolution; add a
"security" key to the ROLE_PERMISSIONS object (alongside existing keys like
"admin", "member", "provision") and populate it with the intended permission
array (either a copy of an existing role's permissions or the exact set required
for security users) so lookups for the "security" role return a defined
permissions array.

Comment on lines +66 to +83
Fourteen new database migrations (`0057`–`0070`) will run automatically on startup. They add:

- Deduped join requests (`0057`)
- Heartbeat run liveness, continuation tracking, and last-useful-action metadata (`0058`)
- Plugin database namespaces (`0059`)
- Issue reference mentions (`0060`)
- Heartbeat scheduled retry tracking (`0061`)
- Routine run dispatch fingerprint and origin-fingerprint uniqueness for routine-execution issues (`0062`)
- Issue thread interactions and idempotency index (`0063`, `0064`)
- Environments and environment leases (`0065`)
- Issue tree holds for subtree pause/cancel/restore (`0066`)
- Agents' default environment foreign key (`0067`)
- Per-company unique local-driver environment index (`0068`)
- Liveness recovery dedupe indexes (`0069`)
- Active-run output watchdog metadata (`0070`)

All migrations are additive — no existing data is modified or removed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate migration files related to 0057-0070 and inspect for non-additive operations.
fd -i '005(7|8|9)|006[0-9]|0070' | while read -r f; do
  echo "=== $f ==="
  rg -n -i 'update\s+|delete\s+|drop\s+(table|column|index)|alter\s+table.*\bdrop\b|truncate\s+' "$f" || true
done

Repository: wopr-network/platform

Length of output: 9861


🏁 Script executed:

cat -n sidecars/paperclip/packages/db/src/migrations/0057_tidy_join_requests.sql

Repository: wopr-network/platform

Length of output: 2050


🏁 Script executed:

cat -n sidecars/paperclip/packages/db/src/migrations/0060_orange_annihilus.sql

Repository: wopr-network/platform

Length of output: 3621


🏁 Script executed:

cat -n sidecars/paperclip/packages/db/src/migrations/0062_routine_run_dispatch_fingerprint.sql

Repository: wopr-network/platform

Length of output: 1043


🏁 Script executed:

cat -n sidecars/paperclip/packages/db/src/migrations/0068_environment_local_driver_unique.sql

Repository: wopr-network/platform

Length of output: 307


Remove or heavily qualify the "no existing data is modified or removed" claim.

Migrations 0057, 0060, 0062, and 0068 contain non-additive operations:

  • 0057 runs two UPDATE statements to reject duplicate pending join requests (by user and email).
  • 0060 executes a DELETE to remove duplicate rows from issue_reference_mentions (keeping the earliest per key).
  • 0062 drops and recreates the issues_open_routine_execution_uq index with a new definition.
  • 0068 drops and recreates the environments_company_driver_idx index.

While these operations appear intentional (deduplication and index refactoring), the statement "no existing data is modified or removed" is inaccurate. Update the release notes to either omit this guarantee or replace it with a specific description of what each migration does.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/releases/v2026.427.0.md` around lines 66 - 83, The release
note's blanket claim "no existing data is modified or removed" is inaccurate;
update the note to either drop that guarantee or explicitly describe
non-additive actions in migrations 0057, 0060, 0062, and 0068: state that 0057
runs UPDATEs to reject duplicate pending join requests, 0060 DELETEs duplicate
rows from issue_reference_mentions (keeping the earliest per key), 0062 drops
and recreates the issues_open_routine_execution_uq index with a new definition,
and 0068 drops and recreates the environments_company_driver_idx index (and thus
may change indexes); keep the rest of the migrations described as additive.

Comment on lines +848 to +853
const checklistAffordanceEnabled = useMemo(
() =>
defaultSortField === "workflow"
&& viewState.groupBy === "none",
[defaultSortField, viewState.groupBy],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Drive the checklist affordance off the active sort, not the default.

This is currently keyed to defaultSortField === "workflow", so the checklist UI disappears when a user manually picks workflow sort, and it can stay visible after they switch away from workflow if the default was workflow.

Suggested fix
   const checklistAffordanceEnabled = useMemo(
     () =>
-      defaultSortField === "workflow"
+      viewState.sortField === "workflow"
       && viewState.groupBy === "none",
-    [defaultSortField, viewState.groupBy],
+    [viewState.groupBy, viewState.sortField],
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const checklistAffordanceEnabled = useMemo(
() =>
defaultSortField === "workflow"
&& viewState.groupBy === "none",
[defaultSortField, viewState.groupBy],
);
const checklistAffordanceEnabled = useMemo(
() =>
viewState.sortField === "workflow"
&& viewState.groupBy === "none",
[viewState.groupBy, viewState.sortField],
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/components/IssuesList.tsx` around lines 848 - 853,
The checklistAffordanceEnabled computation incorrectly checks defaultSortField
instead of the active sort; change the condition to use the current/active sort
field (e.g., viewState.sortField) rather than defaultSortField so the checklist
UI appears only when the user-selected sort is "workflow" and viewState.groupBy
=== "none". Update the useMemo to reference viewState.sortField (or the actual
active sort property on viewState) in place of defaultSortField and keep the
same dependency array.

Comment on lines +72 to +116
const { data: companiesResult = { companies: [], unauthorized: false }, isLoading, error } = useQuery<CompanyListResult>({
queryKey: queryKeys.companies.all,
queryFn: async () => {
try {
return await companiesApi.list();
return { companies: await companiesApi.list(), unauthorized: false };
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
return [];
return { companies: [], unauthorized: true };
}
throw err;
}
},
retry: false,
});
const sidebarCompanies = useMemo(() => companies.filter((company) => company.status !== "archived"), [companies]);
const companies = companiesResult.companies;
const companyListUnauthorized = companiesResult.unauthorized;
const sidebarCompanies = useMemo(
() => companies.filter((company) => company.status !== "archived"),
[companies],
);

// Auto-select first company when list loads
useEffect(() => {
if (companies.length === 0) return;

const selectableCompanies = sidebarCompanies.length > 0 ? sidebarCompanies : companies;
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && selectableCompanies.some((c) => c.id === stored)) return;
if (selectedCompanyId && selectableCompanies.some((c) => c.id === selectedCompanyId)) return;
if (isLoading) return;
if (companies.length === 0) {
if (shouldClearStoredCompanySelection({ companies, isLoading: false, unauthorized: companyListUnauthorized })) {
if (selectedCompanyId !== null) {
setSelectedCompanyIdState(null);
}
localStorage.removeItem(STORAGE_KEY);
}
return;
}

const next = selectableCompanies[0]!.id;
const next = resolveBootstrapCompanySelection({
companies,
sidebarCompanies,
selectedCompanyId,
storedCompanyId: localStorage.getItem(STORAGE_KEY),
});
if (next === null || next === selectedCompanyId) return;
setSelectedCompanyIdState(next);
setSelectionSource("bootstrap");
localStorage.setItem(STORAGE_KEY, next);
}, [companies, selectedCompanyId, sidebarCompanies]);
}, [companies, companyListUnauthorized, isLoading, selectedCompanyId, sidebarCompanies]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Non-401 fetch failures can incorrectly wipe stored selection.

On Line 72, the default { companies: [], unauthorized: false } is used when query data is absent. If the query errors (non-401), the effect can still hit the empty/authorized branch and clear localStorage, which is a data-loss bug during transient outages.

Proposed fix
 export function shouldClearStoredCompanySelection(input: {
   companies: Array<Pick<Company, "id">>;
   isLoading: boolean;
   unauthorized: boolean;
+  hasError: boolean;
 }) {
-  return !input.isLoading && !input.unauthorized && input.companies.length === 0;
+  return !input.isLoading && !input.hasError && !input.unauthorized && input.companies.length === 0;
 }
@@
-      if (shouldClearStoredCompanySelection({ companies, isLoading: false, unauthorized: companyListUnauthorized })) {
+      if (shouldClearStoredCompanySelection({
+        companies,
+        isLoading: false,
+        unauthorized: companyListUnauthorized,
+        hasError: error != null,
+      })) {
@@
-  }, [companies, companyListUnauthorized, isLoading, selectedCompanyId, sidebarCompanies]);
+  }, [companies, companyListUnauthorized, error, isLoading, selectedCompanyId, sidebarCompanies]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/context/CompanyContext.tsx` around lines 72 - 116,
The effect that auto-selects/clears the stored company runs even when the query
errored (non-401), causing localStorage to be wiped on transient failures;
change the useEffect to bail out early when the query has an error (the error
from useQuery) so it only clears selection when we actually have successful
data/authorized empty list—i.e. add a guard like if (error) return; at the top
of the effect (before checking isLoading/companies) so
shouldClearStoredCompanySelection, resolveBootstrapCompanySelection,
localStorage.removeItem(STORAGE_KEY), setSelectedCompanyIdState and
setSelectionSource only run when there is no query error.

import { heartbeatsApi } from "../api/heartbeats";
import { instanceSettingsApi } from "../api/instanceSettings";
import { ApiError } from "../api/client";
import { useHostedMode } from "../hooks/useHostedMode";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== useHostedMode implementation =="
fd -i "useHostedMode" sidecars/paperclip/ui/src/hooks --exec sed -n '1,220p' {}

echo
echo "== Hosted guard before later hooks (pages) =="
rg -n --type=tsx -C2 'const\s*\{\s*isHosted\s*\}\s*=\s*useHostedMode\(\);|if\s*\(isHosted\)\s*return\s*<Navigate' sidecars/paperclip/ui/src/pages

Repository: wopr-network/platform

Length of output: 863


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AgentDetail.tsx hook structure around lines 620-640 =="
sed -n '620,640p' sidecars/paperclip/ui/src/pages/AgentDetail.tsx | cat -n

echo
echo "== Hosted guard pattern across all page files =="
rg -n -A3 'useHostedMode' sidecars/paperclip/ui/src/pages --type ts --type tsx 2>/dev/null || \
  rg -n -A3 'useHostedMode' sidecars/paperclip/ui/src/pages

Repository: wopr-network/platform

Length of output: 14020


Wrap the hosted guard in a wrapper component to avoid breaking the Rules of Hooks.

Line 623 returns before hooks like useParams, useCompany, and others run later. Since useHostedMode uses a query with a 5-minute staleTime, the isHosted value can change between renders. If it does, subsequent hooks become conditional, which violates React's Rules of Hooks. Move the guard to an outer wrapper component and place the rest of the page logic in an inner component where all hooks run unconditionally.

Proposed refactor pattern
 export function AgentDetail() {
   const { isHosted } = useHostedMode();
   if (isHosted) return <Navigate to="/" replace />;
+  return <AgentDetailContent />;
+}
+
+function AgentDetailContent() {
   const { companyPrefix, agentId, tab: urlTab, runId: urlRunId } = useParams<{
     companyPrefix?: string;
     agentId: string;
     tab?: string;
     runId?: string;
   }>();
-  // ...existing AgentDetail body...
+  // ...rest of the original AgentDetail body unchanged...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/pages/AgentDetail.tsx` at line 15, The hosted-mode
early return in AgentDetail.tsx causes conditional hook execution
(useHostedMode, useParams, useCompany, etc.); fix it by extracting the guard
into an outer wrapper component (e.g., HostedGuard or AgentDetailWrapper) that
calls useHostedMode and conditionally renders an inner component (e.g.,
AgentDetailInner) — move all existing hooks and the current component body into
that inner component so useParams, useCompany and all other hooks run
unconditionally, and have the outer wrapper solely decide whether to render the
inner component based on the hosted check.

Comment on lines 123 to +133
export function InstanceExperimentalSettings() {
const { isHosted } = useHostedMode();

// Redirect to home in hosted mode — experimental settings are infrastructure
if (isHosted) return <Navigate to="/" replace />;

const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
const [actionError, setActionError] = useState<string | null>(null);
const [lookbackHoursDraft, setLookbackHoursDraft] = useState("24");
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
const [pendingPreview, setPendingPreview] = useState<IssueGraphLivenessAutoRecoveryPreview | null>(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n sidecars/paperclip/ui/src/pages/InstanceExperimentalSettings.tsx | head -150

Repository: wopr-network/platform

Length of output: 6880


Move the early hosted-mode return before all other hooks to prevent conditional hook execution.

This component violates React's Rules of Hooks by calling useBreadcrumbs, useQueryClient, useState, useEffect, useQuery, and useMutation after a conditional early return. If the isHosted flag changes between renders, React will detect a mismatched hook count and throw an error. The safest fix is to extract the redirect into a tiny wrapper component that checks the flag and returns early, keeping all other hooks in an unconditionally rendered child component.

💡 Safer structure
 export function InstanceExperimentalSettings() {
   const { isHosted } = useHostedMode();
   
-  // Redirect to home in hosted mode — experimental settings are infrastructure
-  if (isHosted) return <Navigate to="/" replace />;
+  if (isHosted) return <Navigate to="/" replace />;
+  return <InstanceExperimentalSettingsContent />;
+}
 
-  const { setBreadcrumbs } = useBreadcrumbs();
+function InstanceExperimentalSettingsContent() {
+  const { setBreadcrumbs } = useBreadcrumbs();
   const queryClient = useQueryClient();
   const [actionError, setActionError] = useState<string | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/src/pages/InstanceExperimentalSettings.tsx` around
lines 123 - 133, The early hosted-mode return in InstanceExperimentalSettings is
causing conditional hook execution; refactor by creating a tiny wrapper that
calls useHostedMode and returns <Navigate to="/" replace /> when isHosted is
true, and move all existing hook usage and JSX into a new child component (e.g.,
InstanceExperimentalSettingsInner) so that useBreadcrumbs, useQueryClient,
useState hooks (lookbackHoursDraft, previewDialogOpen, pendingPreview,
actionError), and any useEffect/useQuery/useMutation calls are always invoked
unconditionally inside that inner component; keep the exported
InstanceExperimentalSettings as the wrapper that only checks isHosted and
renders InstanceExperimentalSettingsInner when not hosted.

Comment on lines +195 to +198
client.setQueryData(queryKeys.instance.experimentalSettings, {
enableIsolatedWorkspaces: true,
enableRoutineTriggers: true,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Seed the full experimental-settings shape here.

InstanceExperimentalSettings now includes additional required fields in this PR, but this fixture still hydrates only a partial object. Any story consumer that reads one of the new flags will get undefined, so the story can drift from real app behavior.

Suggested fix
   client.setQueryData(queryKeys.instance.experimentalSettings, {
+    enableEnvironments: false,
     enableIsolatedWorkspaces: true,
+    autoRestartDevServerWhenIdle: false,
+    enableIssueGraphLivenessAutoRecovery: false,
+    issueGraphLivenessAutoRecoveryLookbackHours: 24,
     enableRoutineTriggers: true,
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
client.setQueryData(queryKeys.instance.experimentalSettings, {
enableIsolatedWorkspaces: true,
enableRoutineTriggers: true,
});
client.setQueryData(queryKeys.instance.experimentalSettings, {
enableEnvironments: false,
enableIsolatedWorkspaces: true,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: false,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
enableRoutineTriggers: true,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sidecars/paperclip/ui/storybook/stories/sub-issues-workflow.stories.tsx`
around lines 195 - 198, The story seeds only a partial
InstanceExperimentalSettings object causing missing flags to be undefined;
update the fixture used in
client.setQueryData(queryKeys.instance.experimentalSettings, ...) to include the
full shape of InstanceExperimentalSettings (add all newly required
boolean/fields introduced in the PR) so consumers read real values; locate the
seed in sub-issues-workflow.stories.tsx and populate every required property on
InstanceExperimentalSettings (matching types) instead of just
enableIsolatedWorkspaces and enableRoutineTriggers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant