Compare commits

..

122 Commits

Author SHA1 Message Date
scotttong
eec4ff855a Phase 4 complete: polish, run-notes, rubric self-check
Consolidates run-a-notes.md from a chronological log into a review
artifact (Header → Decisions → DS deviations → Findings → Deferred →
Rubric alignment → Friction log highlights), and adds
run-a-self-check.md walking each rubric item with pass/fail + reasoning.

Section 5 scoring distinguished into clean-pass vs pass-with-
justification:
- Clean (4): no new tokens, no component extraction, no chart
  tokenization, pause neutral styling (vacuous).
- With justification (4): rounded corners (rounded-full on glyphs,
  rounded-md inherited from shadcn), live-run distinction, raw-
  palette drift (bg-red-400/amber-300/emerald-300 introduced by the
  budget card — mirror BudgetPolicyCard's treatment), no out-of-scope
  changes (additive opt-in extensions to ActivityCharts.tsx that
  leave pages/Dashboard.tsx behaviorally unchanged).

Section 6 qualitative scores are 4/5 across the board with honest
reasoning — deliberately conservative. Improvement is evolutionary
not revolutionary; restraint has two specific decorative choices
(priority-flash highlight, cost-line middot separators) that a
stricter pass would cut; hierarchy has below-hero sequential-section
flatness; live-run's in-card spinner is small relative to the pill.

Preserved commits listed in run-a-notes.md §1 as the full chain from
Phase 2 structural frame through Phase 4 polish.

Two pre-existing untracked files (debug-storybook.log, prototype-ref/)
intentionally left untracked — pre-dates Run A, not in scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:46:19 -07:00
scotttong
873f15d990 Phase 4 polish: suppress chart legend when there's nothing to legend
When an agent has zero runs in the 14-day window, RunActivityChart
renders "No runs yet" text instead of bars. The legend below was still
rendering — looking orphaned and implying a chart exists when it
doesn't. Gate the legend on runs.length > 0 so the empty state is
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:27:32 -07:00
scotttong
ec37967dd3 Phase 4 polish: normalize module heading hierarchy
Three inconsistent patterns for module labels:
- ChartCard title: text-xs font-medium text-muted-foreground
- Costs eyebrow: text-xs text-muted-foreground (missing font-medium)
- In-flight tasks h3: text-sm font-medium (bigger, not muted)

Normalized to the ChartCard pattern — one tier for all module labels,
giving uniform visual rhythm down the page. In-flight's "View all →"
link is also text-xs, so baselines now match cleanly.

Section 6 "Visual restraint" benefit: single type size for module
labels reduces typographic noise; the page reads as one aesthetic
system rather than three.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:26:27 -07:00
scotttong
859523b58b Phase 3d: unified costs
Merge the KPI totals strip and per-run cost table into one coherent
accountability surface. Outer bordered container wraps a summary block
above a borderless table; a single border-t on the table's thead
separates summary from detail.

Structure:
- One container, one outline (Common Region).
- Title "Costs — session" internal, matching ChartCard pattern. The
  external <h3>Costs</h3> in AgentOverview is gone.
- Summary: inline one-liner "$X.XX cumulative · N in · N out · N
  cached" with primary (cost, text-sm font-semibold) and secondary
  (tokens, text-xs muted) hierarchy. Middot separators.
- Table: same columns (Date, Run, Input, Output, Cost), same 10-row
  cap, tabular-nums preserved. Switched rows from border-b+last: to
  border-t for simpler logic; no visual change.

Empty states:
- Neither runtime state nor runs-with-cost → one compact card with
  eyebrow + "No cost data yet."
- Runtime state without runs-with-cost → summary only.
- Runs-with-cost without runtime state → table only (no aggregate
  fallback — per concept §5 honest scoping; session vs all-runs would
  mix vocabularies).
- Both present → both render, separated by the table-header border-t.

Scope preserved:
- CostsSection data sources unchanged — still runtimeState + runs.
- No new queries, hooks, or helpers. runMetrics, formatCents,
  formatTokens, formatDate reused as-is.
- AgentOverview only lost the external Costs <h3> wrapper.

DS deviations: none introduced. All classes already present in the
dashboard region.

Lenses cited in run-a-notes.md: Common Region, Prägnanz, Progressive
Disclosure, Information Scent, Aesthetic-Usability Effect, Jakob's Law.

Findings re-surfaced / carried forward:
- Per-task cost attribution is weak (HeartbeatRun has no reliable
  Issue link). Table shows cost-per-run, not cost-per-task. Relevant
  for rubric Section 2 "Accountability (spend)"; data-model gap, out
  of redesign scope.
- Vocabulary overlap between session-cumulative cost (costs module)
  and month-to-date observed (hero budget). Legible because each is
  explicitly labeled, but fragile.

Typecheck clean. No new tests (no new component, no new helper).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:05:57 -07:00
scotttong
917b72aaf4 Phase 3c: priority affordance
Wire the existing PriorityIcon popover picker into the in-flight tasks
list. Click a priority icon → popover with 4 options → pick → row
updates optimistically, may reposition by the priority-DESC sort, and
briefly highlights.

PriorityIcon already ships the full picker (used in IssueDetail and
IssueProperties). Passing onChange turns its icon into a shadcn Popover
trigger with 4 Button options — keyboard-accessible via Tab/Enter/
Escape. Reused without modification.

Mutation:
- issuesApi.update(id, { priority })
- Optimistic: mutate the dashboard-scoped query cache
  ([...queryKeys.issues.list(companyId), "participant-agent", agentId])
  so inFlightTasks resorts immediately.
- onError: revert snapshot + shadcn error toast with server message.
- onSettled: invalidate queryKeys.issues.list(companyId) so other views
  converge.

Repositioning:
- The existing inFlightTasks memo sorts priority-DESC then updatedAt-DESC.
  Optimistic mutation triggers re-memo → row reposition. No animation
  framework.
- Post-change highlight: 1000ms bg-accent/30 on the changed row via
  local recentlyChangedId state + transition-colors fade. Highlight
  travels with the repositioned row (Zeigarnik / Goal-Gradient).

Nested-interactive concession:
- EntityRow wraps as <Link>; PriorityIcon popover trigger is inside it.
  Wrapped the trigger in a <span> that stopPropagation/preventDefault
  on click + Enter/Space keydown, so icon clicks don't leak to the
  Link. Keyboard focus on the trigger behaves correctly.
- Nested-interactive a11y pattern flagged in run-a-notes — Phase 4
  candidate if we want to refactor EntityRow.

Permissions:
- No client-side gate (matches IssueDetail.tsx pattern — existing app
  behavior is "anyone who can view can edit"). Server rejection
  surfaces via error toast. Flagged in run-a-notes so product knows
  this is deliberate continuity, not silent granting.

DS deviations (pass-with-justification):
- PopoverContent rounded-md inherited from shadcn primitive (accepted
  per Step 0 exemption).
- bg-accent/30 for the highlight — existing DS token + standard Tailwind
  opacity modifier. No new raw-palette drift.

Rubric:
- Section 3 "Change task priority": pass (explicit selector, 4-value
  enum, keyboard-accessible).
- Section 2 "Operations": pass (change priority from dashboard without
  leaving page).
- Section 3 "Navigate to detail": unchanged (Link still routes to
  /issues/{identifier}).

Typecheck clean. Existing ActivityCharts tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:57:49 -07:00
scotttong
0710f166da Phase 3b polish: chart legend and rich tooltips
Pre-existing gap surfaced by chart consolidation. RunActivityChart had
no legend (despite ChartLegend existing in the same file and being
used by PriorityChart/IssueStatusChart) and only a native-title
tooltip showing daily total without the succeeded/failed/other
breakdown.

Scope-preserving implementation:

- Export ChartLegend from ActivityCharts.tsx (previously file-private).
  Rendered inline in AgentOverview as a sibling of RunActivityChart
  inside ChartCard. Zero-touch to the chart component.

- Add opt-in richTooltips?: boolean prop to RunActivityChart. When
  true, each day column is wrapped in a shadcn Tooltip (established
  project pattern) with day + per-category breakdown. When false
  (default), native-title behavior preserved.

Dashboard.tsx (the other consumer) continues to render
RunActivityChart without richTooltips — unchanged behavior there. All
ActivityCharts.tsx changes are additive and backward-compatible.

The rich-tooltip branch uses a <button> (not <div>) as the
TooltipTrigger child for keyboard focusability. cursor-default
preserves visual mouse semantics.

Legend renders all three categories unconditionally for vocabulary
stability across states.

Lenses cited in run-a-notes.md: Recognition over Recall (legend),
Information Scent (breakdown tooltips), Doherty Threshold (shadcn
zero-delay vs native ~500ms), Jakob's Law.

Chart sparseness observation from the smoke test filed as a Phase 4
polish candidate — premature to judge before 3c and 3d land.

Existing ActivityCharts.test.tsx passes unchanged (2 tests green).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:47:47 -07:00
scotttong
51dd51b98a Phase 3b: chart consolidation
Four charts → one. Dropped Issues by Priority, Issues by Status, and
Success Rate from the dashboard composition. Kept RunActivityChart —
the only uniquely-dashboard-shaped temporal signal. Success Rate
merged into the chart card's subtitle as "N% success · Last 14 days."

Reachability for dropped chart data:
- Issues by Priority/Status → /issues?participantAgentId=<id> via
  the existing "View all →" link, then filter. Two interactions,
  satisfies rubric Section 4.
- Success Rate → visible as subtitle on the surviving card (zero
  interactions).

Layout collapses from grid-cols-4 to a single full-width ChartCard.
No wrapping grid; ChartCard is already block-level. No chart
tokenization (Step 0 policy preserved). No new wrappers.

Components still exported from ActivityCharts.tsx because
pages/Dashboard.tsx uses them. Scope-respecting: only removed from
AgentDetail.tsx imports and JSX.

Success-rate formula: succeeded/total — matches the stacked bars
visually, same as the dropped SuccessRateChart.

All decisions, lenses cited (Information Scent, Hick's Law, Pareto,
Prägnanz), and reachability checks captured in run-a-notes.md.

Carried forward to Phase 4: Latest Run card right edge doesn't align
with the content grid right edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:39:56 -07:00
scotttong
774679bf50 Phase 3a polish round 3: hero-spanning activity pill
Lifts the activity pill out of the left zone and promotes it to a
hero-level header above the 75/25 grid. The Latest Run card and
budget card now share a baseline; the prior top-of-zone mismatch is
gone.

Mode 2 alignment exploration resolved in favor of Option C:
- Semantic honesty — the pill describes the agent, not the
  current-work column. Moving it up reflects its actual scope.
- Von Restorff preserved — the cyan signal keeps its spatial
  isolation, important for rubric Section 6 "Live-run signal
  strength."
- Zero structural cost — no new elements or tokens; the pill changes
  position, nothing else changes.
- Latest Run card meaning sharpens — now unambiguously "a specific
  run" rather than conflating agent state.
- Zero-runs gracefully handled without conditional complexity.

Pill-grounding treatment (chosen to avoid the "lonely pill above a
wide grid" risk): tight-proximity via space-y-3 (12px) on the hero
wrapper. No border-b separator — a horizontal rule would read as a
section divider, heavier than the pill deserves. Gestalt Proximity
groups the pill with the grid below without needing a visual
connector. Aesthetic-Usability Effect: restraint.

Structure:
- Hero wrapper: outer div with space-y-3.
- Child 1: activity pill (flex row, dot + label).
- Child 2: the existing grid-cols-[3fr_1fr] with left + right zones.
  Left zone's internal space-y-3 unchanged; now starts directly with
  Latest Run card.

1440px no-scroll: confirmed still passing. Delta is zero to within
rounding — the ~20px gained at the hero top is offset by the ~20px
left zone shrinks.

All decisions and operating-principles citations captured in
run-a-notes.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:34:28 -07:00
scotttong
4996295b07 Phase 3a polish round 2: compact run feed replaces icon strip
Concept-level revision. The icon-strip treatment from concept §7 was
compact but information-sparse — run id, outcome detail, timestamp all
hidden behind hover. Replaced with a 3-row compact feed below the
Latest Run card; information readable at a glance.

Layout
- Hero back to two-zone: 75/25 (lg:grid-cols-[3fr_1fr]).
- Left zone (75%): activity pill + Latest Run card + prior-runs feed +
  idle hint.
- Right zone (25%): budget card alone.
- Three-zone layout from polish round 1 collapsed back — feed needs
  card width for tabular-aligned rows.

Prior-runs feed
- Three one-line rows in a bordered container, border-t between rows.
- Each row: colored status icon (h-3 w-3, shape+color per runStatusIcons),
  mono run id (first 8 chars), plain status label, right-aligned
  relative time. Entire row is a Link.
- Rows are the 3 runs before the Latest Run card's pick — filtered by
  id so Latest Run isn't duplicated.
- No tooltips — all info visible in the row.

LatestRunCard refactor
- Signature changed from { runs, agentId } to { run, agentId }. Caller
  (AgentOverview) now owns the sort/liveRun/latestRun/priorRuns
  computation via a single sortedRuns memo. Simpler component; prevents
  duplication with the prior-runs feed.

Jakob's Law, Recognition over Recall, and Information Scent cited in
run-a-notes.md. Concept doc gets a "Phase 3a revision" note recording
the drift from §7 — original §7 preserved as authored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:55:55 -07:00
scotttong
568e1c15df Phase 3a polish: 50/25/25 hero layout and shadcn tooltips
Two follow-ups from the 3a smoke-test.

Layout: 50/50 right zone still felt cramped with budget + recent-runs
stacked. Split into 50/25/25 — left stays for current-work, middle 25%
is budget alone, right 25% is recent-runs alone. Each peer gets
horizontal breathing room. Budget card's dense `$X of $Y` + `N%` line
verified to fit at ~275px content width with `justify-between` and
`gap-2`; worst-case values still fit.

Tooltips: native HTML `title` on the recent-runs <Link> was unreliable
(delay + styling OS-dependent, sometimes suppressed on anchors).
Replaced with shadcn Tooltip primitive — already project-standard
(TooltipProvider mounted globally in main.tsx, used elsewhere e.g.
HintIcon in agent-config-primitives). Content now carries the full
concept-specified triplet: run id + outcome + relative time.
aria-label retained for screen readers.

Decisions + findings captured in run-a-notes.md Phase 3a polish section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:06:13 -07:00
scotttong
6b0f407943 Phase 3a: hero module redesign
Rebuild the load-bearing hero surface per concept §1 with full styling
attention. Adds the "Next up" forward-looking signal the concept flagged,
relocates cyan to the activity pill, and rebuilds the recent-runs strip
for WCAG color-independence.

Structure
- Hero grid: 55/45 → 50/50 (grid-cols-2 at lg:). Right zone no longer
  cramped; both zones read as parallel questions per Common Region.
- Left zone: activity pill + current-work card + idle "Next up" line.
- Right zone: budget card + recent-runs icon strip.

Activity-state pill
- Minimal dot + text; no pill container. Text color `text-foreground`
  default, overridden to cyan-600 (running) or red-600 (error). The dot
  carries severity for paused/pending_approval/terminated. All classes
  already present in the dashboard region — no raw-palette drift.
- Passes `active` through as a distinct state rather than collapsing to
  idle; matches the DS dot catalog.

LatestRunCard
- Stripped the header row (redundant "Live Run"/"Latest Run" label,
  pulse dot, and separate "View details →" link). The card itself is
  the only Link; click anywhere navigates. aria-label on the Link
  restores the screen-reader context.
- Removed card-level `border-cyan-500/30` + glow shadow. The activity
  pill above carries the external liveness signal; the internal
  StatusIcon (cyan spinning Loader2) is the local signal. Eliminates
  the three-way cyan duplication.
- Moved the early return below the hooks to preserve hook ordering
  while handling the zero-runs case cleanly in the caller.

Idle "Next up" hint
- When activityStatus is `idle` or `active` and in-flight tasks exist,
  renders a one-line link to the top-priority task. When there are
  zero in-flight tasks, renders "No pending work". Derived from the
  existing inFlightTasks memo — no new data fetches.

Recent-runs strip
- Replaced dots with colored lucide icons (CheckCircle2, XCircle,
  Loader2, Clock, etc.) from runStatusIcons, at h-3.5 w-3.5. Each icon
  is a Link with aria-label + title. Shape + color gives WCAG
  color-independence. Removes the Phase 2 `text-*.replace(/text-/g, "bg-")`
  hack.

Budget card
- Extracted "resets in N days" label into a useMemo for cleaner JSX.
  Behavior unchanged from the Phase 2 fix.

All decisions, DS deviations, and surfaced findings captured in
projects/agent-profile-dashboard/run-a-notes.md with operating-principles
citations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:23:20 -07:00
scotttong
389f8105ac Phase 2 fix: derive dashboard activity-state from runs, complete budget window
Two issues surfaced by the 1440px smoke test on the Phase 2 structural frame:

1. The hero activity-state pill read "Idle" while the Live Run card below
   read "Running" with a cyan pulse — the dashboard was contradicting itself
   during live runs. Root cause: agent.status drifts stale in the cache
   (queryKey mismatch between the URL-ref-keyed detail query and the
   UUID-keyed agent.status live-event invalidation). Fix: derive
   running-ness from the runs array — the same signal the live-run card
   trusts — rather than reading agent.status directly. Resolution order is
   paused > pending_approval > terminated > running(derived) > error >
   idle, so agent-level terminal states still take precedence.

2. Budget card was missing the "resets in N days" window copy the concept
   specified. Added using the existing budgetSummary.windowEnd; suppressed
   when the synthesized-fallback policy is in use (policyId === "") since
   its windowEnd is meaningless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:02:01 -07:00
scotttong
78eed3b197 Phase 2: Agent Profile Dashboard structural frame
Stub-level structural implementation of the concept-approved frame:

- Thread agentBudgetSummary into AgentOverview as optional prop
- Top-level layout rewrite: two-zone hero | chart band | in-flight
  tasks | costs
- Hero left zone: activity-state dot + agent.status label; existing
  LatestRunCard as the current-work card (unchanged)
- Hero right zone: compact budget-position card (progress bar mirrors
  BudgetPolicyCard treatment); 7-dot recent-runs strip, each linking
  to its run detail
- In-flight tasks: filter status in {todo, in_progress, blocked},
  sort by priority DESC then updatedAt DESC, limit 7, read-only
  PriorityIcon as the Phase 3c priority-affordance placeholder
- Chart band and CostsSection carried over unchanged (Phase 3b/3d)

No visual polish, no empty-state work, no chart merges, no priority
mutation wired. This commit lands the structural frame only so the
1440px no-scroll smoke test can happen at the Phase 2 checkpoint.
2026-04-23 01:48:22 -07:00
scotttong
aecf32e60b Add Run A concept document 2026-04-23 01:43:25 -07:00
scotttong
2ede0af41f Add Run A execution plan
Five-phase structure: concept, structural frame, module-level
redesign (4 sub-modules), polish + rubric self-check.
Mandatory checkpoints after concept, after structural frame,
after each Phase 3 sub-module, and at final review.
2026-04-23 01:20:23 -07:00
scotttong
31e9a92853 Add Run A discovery document
Structural analysis of AgentOverview, LatestRunCard, CostsSection
plus data-model reality check for the three brief action affordances.
Surfaced three brief edits now landed on shared baseline.
2026-04-23 01:20:23 -07:00
scotttong
0f5895e4e5 Add shared UX operating principles for Run A and Run B
Both paths work from the same UX operating manual — role definition,
design lenses, visual quality bar, DS-first discipline, working rules.
Path-specific operational mechanics are not in this document.

Isolating the shared principles removes a confound from the experiment:
any differences between Run A and Run B outputs reflect execution-path
differences, not differences in design guidance.
2026-04-23 01:20:18 -07:00
scotttong
2f9f657df5 Remove stale pause references from Agent Profile Dashboard brief
Pause is handled by existing page-chrome control and is out of scope
for this redesign (per earlier brief update). DS-policy bullet and
Known deferrals section were vestigial references to pause styling
that no longer apply. The --signal-warning deferral is documented
in tokens-review.md where it belongs.
2026-04-23 01:02:04 -07:00
scotttong
53db6095b0 Update Agent Profile Dashboard brief and rubric based on Run A discovery
Discovery surfaced three brief-vs-reality gaps that required edits before
either run proceeds:

- Backend does not support arbitrary task reordering. Action reframed
  from "reorder in-flight tasks" to "change task priority."
- Pause is already fully implemented in page chrome (PauseResumeButton).
  Removed from dashboard's in-scope actions to avoid duplication.
- Live economics requirement clarified: both budget position and
  session burn rate required on dashboard; budget position specifically
  satisfies the monitoring-frame rubric test.

These edits preserve experiment symmetry — both Run A and Run B work
from the updated baseline.
2026-04-23 00:47:24 -07:00
scotttong
3907d6dc40 Add brief, rubric, and reference screenshots for Agent Profile Dashboard experiment
Shared baseline for Run A (Claude Code) and Run B (Paperclip) two-path
experiment on the Dashboard tab of the Agent Profile page.

- projects/agent-profile-dashboard/brief.md — what the redesign is
- projects/agent-profile-dashboard/rubric.md — how to evaluate success
- projects/agent-profile-dashboard/reference/ — screenshots of current state

Both run branches will fast-forward to this commit so both runs start
from identical shared baseline.
2026-04-22 17:53:06 -07:00
scotttong
1d74e78a96 ds: token cleanup, radius monotonic restore, plugin SDK contract tagging
Five source-code changes tied to the DS decision lock-ins (doc/design-system/REVIEW.md):

1. Signal tokens added in ui/src/index.css. --signal-success (oklch green-700/600) and --signal-success-foreground (white) paired with --destructive as the DS's action-severity vocabulary. Sourced from the canonical approve-button treatment (ApprovalCard, ApprovalDetail, Inbox). No call sites migrated. Tailwind aliases exposed via @theme inline.

2. destructive-foreground light-mode value fixed. Was oklch(0.577 0.245 27.325) (equalled --destructive — would render invisible); now oklch(0.985 0 0) (white, matching dark mode). Zero production consumers; safe change.

3. Radius scale restored to monotonic. --radius-lg: 0px → 0.625rem (10px) and --radius-xl: 0px → 0.75rem (12px). 226 call sites across 58 files in ui/src/ migrated from rounded-lg/rounded-xl to rounded-none to preserve the existing flat-Swiss aesthetic on dashboard surfaces. Shadcn primitives excluded — ui/src/components/ui/dialog.tsx retains rounded-lg so DialogContent now renders with real 10px-rounded corners (the first observable visual change from the radius work, intentional). One test assertion updated in lockstep (ProjectWorkspaceSummaryCard.test.tsx:135).

4. 13 dead tokens documented as reserved. --chart-1..5 and all 8 --sidebar-* tokens kept in index.css with JSDoc-style comments explaining their reserved/aspirational status (chart = future chart tokenization; sidebar = shadcn sidebar primitive compatibility).

5. Plugin SDK hybrid status made visible. 9 unimplemented contract components in packages/plugins/sdk/src/ui/components.ts now carry @status contract-only JSDoc tags so plugin authors see the status in IDE tooltips at call sites. MetricCard and StatusBadge are implemented; the other 9 will fail at runtime if rendered. Prioritization deferred to a separate plugin-SDK roadmap conversation.

Explicitly not in scope: status-colors.ts tokenization, chart color tokenization, component merges, renames, --signal-warning/--signal-info variants, elevation/motion/spacing tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:34:53 -07:00
scotttong
0875aa54ff ds: extract design system documentation under doc/design-system/
Four-stage design system extraction (ds-discovery + ds-extraction skills):
- _discovery.json — styling stack, token sources, component layout, Storybook inventory
- tokens/ — canonical token inventory (37 tokens across color + radius), machine-readable tokens.json, drift review
- components/ — 135-component index with status markers, 53 per-component detail files, cross-cutting review (duplicates, naming, token non-compliance, story gaps, plugin SDK contract status)
- patterns/ — 10 pattern docs (list-page, detail-page, sidebar-chrome, finance-card, entity-properties-panel, entity-creation-dialog, status-display, entity-row, subscription-panel, quota-display), variance analysis, pattern opportunities
- REVIEW.md — entry point ordered by expected human value; recommended review order with time estimates; confidence levels; scope limitations

All extraction decisions captured in place. Components-review.md structured as a persistent backlog so future developers can pick any section up cold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:34:29 -07:00
Dotta
a26e1288b6 [codex] Polish issue board workflows (#4224)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Human operators supervise that work through issue lists, issue
detail, comments, inbox groups, markdown references, and
profile/activity surfaces
> - The branch had many small UI fixes that improve the operator loop
but do not need to ship with backend runtime migrations
> - These changes belong together as board workflow polish because they
affect scanning, navigation, issue context, comment state, and markdown
clarity
> - This pull request groups the UI-only slice so it can merge
independently from runtime/backend changes
> - The benefit is a clearer board experience with better issue context,
steadier optimistic updates, and more predictable keyboard navigation

## What Changed

- Improves issue properties, sub-issue actions, blocker chips, and issue
list/detail refresh behavior.
- Adds blocker context above the issue composer and stabilizes
queued/interrupted comment UI state.
- Improves markdown issue/GitHub link rendering and opens external
markdown links in a new tab.
- Adds inbox group keyboard navigation and fold/unfold support.
- Polishes activity/avatar/profile/settings/workspace presentation
details.

## Verification

- `pnpm exec vitest run ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownBody.test.tsx ui/src/lib/inbox.test.ts
ui/src/lib/optimistic-issue-comments.test.ts`

## Risks

- Low to medium risk: changes are UI-focused but cover high-traffic
issue and inbox surfaces.
- This branch intentionally does not include the backend runtime changes
from the companion PR; where UI calls newer API filters, unsupported
servers should continue to fail visibly through existing API error
handling.
- Visual screenshots were not captured in this heartbeat; targeted
component/helper tests cover the changed behavior.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5-based coding agent runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat environment.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-21 12:25:34 -05:00
Dotta
09d0678840 [codex] Harden heartbeat scheduling and runtime controls (#4223)
## Thinking Path

> - Paperclip orchestrates AI agents through issue checkout, heartbeat
runs, routines, and auditable control-plane state
> - The runtime path has to recover from lost local processes, transient
adapter failures, blocked dependencies, and routine coalescing without
stranding work
> - The existing branch carried several reliability fixes across
heartbeat scheduling, issue runtime controls, routine dispatch, and
operator-facing run state
> - These changes belong together because they share backend contracts,
migrations, and runtime status semantics
> - This pull request groups the control-plane/runtime slice so it can
merge independently from board UI polish and adapter sandbox work
> - The benefit is safer heartbeat recovery, clearer runtime controls,
and more predictable recurring execution behavior

## What Changed

- Adds bounded heartbeat retry scheduling, scheduled retry state, and
Codex transient failure recovery handling.
- Tightens heartbeat process recovery, blocker wake behavior, issue
comment wake handling, routine dispatch coalescing, and
activity/dashboard bounds.
- Adds runtime-control MCP tools and Paperclip skill docs for issue
workspace runtime management.
- Adds migrations `0061_lively_thor_girl.sql` and
`0062_routine_run_dispatch_fingerprint.sql`.
- Surfaces retry state in run ledger/agent UI and keeps related shared
types synchronized.

## Verification

- `pnpm exec vitest run
server/src/__tests__/heartbeat-retry-scheduling.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/routines-service.test.ts`
- `pnpm exec vitest run src/tools.test.ts` from `packages/mcp-server`

## Risks

- Medium risk: this touches heartbeat recovery and routine dispatch,
which are central execution paths.
- Migration order matters if split branches land out of order: merge
this PR before branches that assume the new runtime/routine fields.
- Runtime retry behavior should be watched in CI and in local operator
smoke tests because it changes how transient failures are resumed.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5-based coding agent runtime, shell/git tool use
enabled. Exact hosted model build and context window are not exposed in
this Paperclip heartbeat environment.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-21 12:24:11 -05:00
Dotta
ab9051b595 Add first-class issue references (#4214)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Operators and agents coordinate through company-scoped issues,
comments, documents, and task relationships.
> - Issue text can mention other tickets, but those references were
previously plain markdown/text without durable relationship data.
> - That made it harder to understand related work, surface backlinks,
and keep cross-ticket context visible in the board.
> - This pull request adds first-class issue reference extraction,
storage, API responses, and UI surfaces.
> - The benefit is that issue references become queryable, navigable,
and visible without relying on ad hoc text scanning.

## What Changed

- Added shared issue-reference parsing utilities and exported
reference-related types/constants.
- Added an `issue_reference_mentions` table, idempotent migration DDL,
schema exports, and database documentation.
- Added server-side issue reference services, route integration,
activity summaries, and a backfill command for existing issue content.
- Added UI reference pills, related-work panels, markdown/editor mention
handling, and issue detail/property rendering updates.
- Added focused shared, server, and UI tests for parsing, persistence,
display, and related-work behavior.
- Rebased `PAP-735-first-class-task-references` cleanly onto
`public-gh/master`; no `pnpm-lock.yaml` changes are included.

## Verification

- `pnpm -r typecheck`
- `pnpm test:run packages/shared/src/issue-references.test.ts
server/src/__tests__/issue-references-service.test.ts
ui/src/components/IssueRelatedWorkPanel.test.tsx
ui/src/components/IssueProperties.test.tsx
ui/src/components/MarkdownBody.test.tsx`

## Risks

- Medium risk because this adds a new issue-reference persistence path
that touches shared parsing, database schema, server routes, and UI
rendering.
- Migration risk is mitigated by `CREATE TABLE IF NOT EXISTS`, guarded
foreign-key creation, and `CREATE INDEX IF NOT EXISTS` statements so
users who have applied an older local version of the numbered migration
can re-run safely.
- UI risk is limited by focused component coverage, but reviewers should
still manually inspect issue detail pages containing ticket references
before merge.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5-based coding agent, tool-using shell workflow with
repository inspection, git rebase/push, typecheck, and focused Vitest
verification.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-21 10:02:52 -05:00
Dotta
1954eb3048 [codex] Detect issue graph liveness deadlocks (#4209)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat harness is responsible for waking agents, reconciling
issue state, and keeping execution moving.
> - Some dependency graphs can become live-locks when a blocked issue
depends on an unassigned, cancelled, or otherwise uninvokable issue.
> - Review and approval stages can also stall when the recorded
participant can no longer be resolved.
> - This pull request adds issue graph liveness classification plus
heartbeat reconciliation that creates durable escalation work for those
cases.
> - The benefit is that harness-level deadlocks become visible,
assigned, logged, and recoverable instead of silently leaving task
sequences blocked.

## What Changed

- Added an issue graph liveness classifier for blocked dependency and
invalid review participant states.
- Added heartbeat reconciliation that creates one stable escalation
issue per liveness incident, links it as a blocker, comments on the
affected issue, wakes the recommended owner, and logs activity.
- Wired startup and periodic server reconciliation for issue graph
liveness incidents.
- Added focused tests for classifier behavior, heartbeat escalation
creation/deduplication, and queued dependency wake promotion.
- Fixed queued issue wakes so a coalesced wake re-runs queue selection,
allowing dependency-unblocked work to start immediately.

## Verification

- `pnpm exec vitest run
server/src/__tests__/heartbeat-dependency-scheduling.test.ts
server/src/__tests__/issue-liveness.test.ts
server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts`
- Passed locally: `server/src/__tests__/issue-liveness.test.ts` (5
tests)
- Skipped locally: embedded Postgres suites because optional package
`@embedded-postgres/darwin-x64` is not installed on this host
- `pnpm --filter @paperclipai/server typecheck`
- `git diff --check`
- Greptile review loop: ran 3 times as requested; the final
Greptile-reviewed head `0a864eab` had 0 comments and all Greptile
threads were resolved. Later commits are CI/test-stability fixes after
the requested max Greptile pass count.
- GitHub PR checks on head `87493ed4`: `policy`, `verify`, `e2e`, and
`security/snyk (cryppadotta)` all passed.

## Risks

- Moderate operational risk: the reconciler creates escalation issues
automatically, so incorrect classification could create noise. Stable
incident keys and deduplication limit repeated escalation.
- Low schema risk: this uses existing issue, relation, comment, wake,
and activity log tables with no migration.
- No UI screenshots included because this change is server-side harness
behavior only.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5-based coding agent. Exact runtime model ID and
context window were not exposed in this session. Used tool execution for
git, tests, typecheck, Greptile review handling, and GitHub CLI
operations.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-21 09:11:12 -05:00
Robin van Duiven
8d0c3d2fe6 fix(hermes): inject agent JWT into Hermes adapter env to fix identity attribution (#3608)
## Thinking Path

> - Paperclip orchestrates AI agents and records their actions through
auditable issue comments and API writes.
> - The local adapter registry is responsible for adapting each agent
runtime to Paperclip's server-side execution context.
> - The Hermes local adapter delegated directly to
`hermes-paperclip-adapter`, whose current execution context type
predates the server `authToken` field.
> - Without explicitly passing the run-scoped agent token and run id
into Hermes, Hermes could inherit a server or board-user
`PAPERCLIP_API_KEY` and lack a usable `PAPERCLIP_RUN_ID` for mutating
API calls.
> - That made Paperclip writes from Hermes agents risk appearing under
the wrong identity or without the correct run-scoped attribution.
> - This pull request wraps the Hermes execution call so Hermes receives
the agent run JWT as `PAPERCLIP_API_KEY` and the current execution id as
`PAPERCLIP_RUN_ID` while preserving explicit adapter configuration where
appropriate.
> - Follow-up review fixes preserve Hermes' built-in prompt when no
custom prompt template exists and document the intentional type cast.
> - The benefit is reliable agent attribution for the covered local
Hermes path without clobbering Hermes' default heartbeat/task
instructions.

## What Changed

- Wrapped `hermesLocalAdapter.execute` so `ctx.authToken` is injected
into `adapterConfig.env.PAPERCLIP_API_KEY` when no explicit Paperclip
API key is already configured.
- Injected `ctx.runId` into `adapterConfig.env.PAPERCLIP_RUN_ID` so the
auth guard's `X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID` instruction
resolves to the current run id.
- Added a Paperclip API auth guard to existing custom Hermes
`promptTemplate` values without creating a replacement prompt when no
custom template exists.
- Documented the intentional `as unknown as` cast needed until
`hermes-paperclip-adapter` ships an `AdapterExecutionContext` type that
includes `authToken`.
- Added registry tests for JWT injection, run-id injection, explicit key
preservation, default prompt preservation, and the no-`authToken`
early-return path.

## Verification

- [x] `pnpm --filter "./server" exec vitest run adapter-registry` - 8
tests passed.
- [x] `pnpm --filter "./server" typecheck` - passed.
- [x] Trigger a Hermes agent heartbeat and verify Paperclip writes
appear under the agent identity rather than a shared board-user
identity, with the correct run id on mutating requests.

## Risks

- Low migration risk: this changes only the Hermes local adapter wrapper
and tests.
- Existing explicit `adapterConfig.env.PAPERCLIP_API_KEY` values are
preserved to avoid breaking intentionally configured agents.
- `PAPERCLIP_RUN_ID` is set from `ctx.runId` for each execution so
mutating API calls use the current run id instead of a stale or literal
placeholder value.
- Prompt behavior is intentionally conservative: the auth guard is only
prepended when a custom prompt template already exists, so Hermes'
built-in default prompt remains intact for unconfigured agents.
- Remaining operational risk: the identity and run-id behavior should
still be verified with a live Hermes heartbeat before relying on it in
production.

## Model Used

- OpenAI Codex, GPT-5 family coding agent, tool use enabled for local
shell, GitHub CLI, and test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (not applicable: backend-only change)
- [x] I have updated relevant documentation to reflect my changes (not
applicable: no product docs changed; PR description updated)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Dotta <bippadotta@protonmail.com>
2026-04-21 07:18:11 -05:00
Dotta
1266954a4e [codex] Make heartbeat scheduling blocker-aware (#4157)
## Thinking Path

> - Paperclip orchestrates AI agents through issue-driven heartbeats,
checkouts, and wake scheduling.
> - This change sits in the server heartbeat and issue services that
decide which queued runs are allowed to start.
> - Before this branch, queued heartbeats could be selected even when
their issue still had unresolved blocker relationships.
> - That let blocked descendant work compete with actually-ready work
and risked auto-checking out issues that were not dependency-ready.
> - This pull request teaches the scheduler and checkout path to consult
issue dependency readiness before claiming queued runs.
> - It also exposes dependency readiness in the agent inbox so agents
can see which assigned issues are still blocked.
> - The result is that heartbeat execution follows the DAG of blocked
dependencies instead of waking work out of order.

## What Changed

- Added `IssueDependencyReadiness` helpers to `issueService`, including
unresolved blocker lookup for single issues and bulk issue lists.
- Prevented issue checkout and `in_progress` transitions when unresolved
blockers still exist.
- Made heartbeat queued-run claiming and prioritization dependency-aware
so ready work starts before blocked descendants.
- Included dependency readiness fields in `/api/agents/me/inbox-lite`
for agent heartbeat selection.
- Added regression coverage for dependency-aware heartbeat promotion and
issue-service participation filtering.

## Verification

- `pnpm run preflight:workspace-links`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-dependency-scheduling.test.ts
server/src/__tests__/issues-service.test.ts`
- On this host, the Vitest command passed, but the embedded-Postgres
portions of those files were skipped because
`@embedded-postgres/darwin-x64` is not installed.

## Risks

- Scheduler ordering now prefers dependency-ready runs, so any hidden
assumptions about strict FIFO ordering could surface in edge cases.
- The new guardrails reject checkout or `in_progress` transitions for
blocked issues; callers depending on the old permissive behavior would
now get `422` errors.
- Local verification did not execute the embedded-Postgres integration
paths on this macOS host because the platform binary package was
missing.

> I checked `ROADMAP.md`; this is a targeted execution/scheduling fix
and does not duplicate planned roadmap feature work.

## Model Used

- OpenAI Codex via the Paperclip `codex_local` adapter in this
workspace. Exact backend model ID is not surfaced in the runtime here;
tool-enabled coding agent with terminal execution and repository editing
capabilities.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-20 16:03:57 -05:00
Hiuri Noronha
1bf2424377 fix: honor Hermes local command override (#3503)
## Summary

This fixes the Hermes local adapter so that a configured command
override is respected during both environment tests and execution.

## Problem

The Hermes adapter expects `adapterConfig.hermesCommand`, but the
generic local command path in the UI was storing
`adapterConfig.command`.

As a result, changing the command in the UI did not reliably affect
runtime behavior. In real use, the adapter could still fall back to the
default `hermes` binary.

This showed up clearly in setups where Hermes is launched through a
wrapper command rather than installed directly on the host.

## What changed

- switched the Hermes local UI adapter to the Hermes-specific config
builder
- updated the configuration form to read and write `hermesCommand` for
`hermes_local`
- preserved the override correctly in the test-environment path
- added server-side normalization from legacy `command` to
`hermesCommand`

## Compatibility

The server-side normalization keeps older saved agent configs working,
including configs that still store the value under `command`.

## Validation

Validated against a Docker-based Hermes workflow using a local wrapper
exposed through a symlinked command:

- `Command = hermes-docker`
- environment test respects the override
- runs no longer fall back to `hermes`

Typecheck also passed for both UI and server.

Co-authored-by: NoronhaH <NoronhaH@users.noreply.github.com>
2026-04-20 15:55:08 -05:00
LeonSGP
51f127f47b fix(hermes): stop advertising unsupported instructions bundles (#3908)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Local adapter capability flags decide which configuration surfaces
the UI and server expose for each adapter.
> - `hermes_local` currently advertises managed instructions bundle
support, so Paperclip exposes the AGENTS.md bundle flow for Hermes
agents.
> - The bundled `hermes-paperclip-adapter` only consumes
`promptTemplate` at runtime and does not read `instructionsFilePath`, so
that advertised bundle path silently does nothing.
> - Issue #3833 reports exactly that mismatch: users configure AGENTS.md
instructions, but Hermes only receives the built-in heartbeat prompt.
> - This pull request stops advertising managed instructions bundles for
`hermes_local` until the adapter actually consumes bundle files at
runtime.

## What Changed

- Changed the built-in `hermes_local` server adapter registration to
report `supportsInstructionsBundle: false`.
- Updated the UI's synchronous built-in capability fallback so Hermes no
longer shows the managed instructions bundle affordance on first render.
- Added regression coverage in
`server/src/__tests__/adapter-routes.test.ts` to assert that
`hermes_local` still reports skills + local JWT support, but not
instructions bundle support.

## Verification

- `git diff --check`
- `node --experimental-strip-types --input-type=module -e "import {
findActiveServerAdapter } from './server/src/adapters/index.ts'; const
adapter = findActiveServerAdapter('hermes_local');
console.log(JSON.stringify({ type: adapter?.type,
supportsInstructionsBundle: adapter?.supportsInstructionsBundle,
supportsLocalAgentJwt: adapter?.supportsLocalAgentJwt, supportsSkills:
Boolean(adapter?.listSkills || adapter?.syncSkills) }));"`
- Observed
`{"type":"hermes_local","supportsInstructionsBundle":false,"supportsLocalAgentJwt":true,"supportsSkills":true}`
- Added adapter-routes regression assertions for the Hermes capability
contract; CI should validate the full route path in a clean workspace.

## Risks

- Low risk: this only changes the advertised capability surface for
`hermes_local`.
- Behavior change: Hermes agents will no longer show the broken managed
instructions bundle UI until the underlying adapter actually supports
`instructionsFilePath`.
- Existing Hermes skill sync and local JWT behavior are unchanged.

## Model Used

- OpenAI Codex, GPT-5.4 class coding agent, medium reasoning,
terminal/git/gh tool use.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-20 15:54:14 -05:00
github-actions[bot]
b94f1a1565 chore(lockfile): refresh pnpm-lock.yaml (#4139)
Auto-generated lockfile refresh after dependencies changed on master.
This PR only updates pnpm-lock.yaml.

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-04-20 12:15:59 -05:00
Dotta
2de893f624 [codex] add comprehensive UI Storybook coverage (#4132)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - The board UI is the main operator surface, so its component and
workflow coverage needs to stay reviewable as the product grows.
> - This branch adds Storybook as a dedicated UI reference surface for
core Paperclip screens and interaction patterns.
> - That work spans Storybook infrastructure, app-level provider wiring,
and a large fixture set that can render real control-plane states
without a live backend.
> - The branch also expands coverage across agents, budgets, issues,
chat, dialogs, navigation, projects, and data visualization so future UI
changes have a concrete visual baseline.
> - This pull request packages that Storybook work on top of the latest
`master`, excludes the lockfile from the final diff per repo policy, and
fixes one fixture contract drift caught during verification.
> - The benefit is a single reviewable PR that adds broad UI
documentation and regression-surfacing coverage without losing the
existing branch work.

## What Changed

- Added Storybook 10 wiring for the UI package, including root scripts,
UI package scripts, Storybook config, preview wrappers, Tailwind
entrypoints, and setup docs.
- Added a large fixture-backed data source for Storybook so complex
board states can render without a live server.
- Added story suites covering foundations, status language,
control-plane surfaces, overview, UX labs, agent management, budget and
finance, forms and editors, issue management, navigation and layout,
chat and comments, data visualization, dialogs and modals, and
projects/goals/workspaces.
- Adjusted several UI components for Storybook parity so dialogs, menus,
keyboard shortcuts, budget markers, markdown editing, and related
surfaces render correctly in isolation.
- Rebasing work for PR assembly: replayed the branch onto current
`master`, removed `pnpm-lock.yaml` from the final PR diff, and aligned
the dashboard fixture with the current `DashboardSummary.runActivity`
API contract.

## Verification

- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/ui build-storybook`
- Manual diff audit after rebase: verified the PR no longer includes
`pnpm-lock.yaml` and now cleanly targets current `master`.
- Before/after UI note: before this branch there was no dedicated
Storybook surface for these Paperclip views; after this branch the local
Storybook build includes the new overview and domain story suites in
`ui/storybook-static`.

## Risks

- Large static fixture files can drift from shared types as dashboard
and UI contracts evolve; this PR already needed one fixture correction
for `runActivity`.
- Storybook bundle output includes some large chunks, so future growth
may need chunking work if build performance becomes an issue.
- Several component tweaks were made for isolated rendering parity, so
reviewers should spot-check key board surfaces against the live app
behavior.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in the Paperclip harness; exact
serving model ID is not exposed in-runtime to the agent.
- Tool-assisted workflow with terminal execution, git operations, local
typecheck/build verification, and GitHub CLI PR creation.
- Context window/reasoning mode not surfaced by the harness.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 12:13:23 -05:00
Dotta
7a329fb8bb Harden API route authorization boundaries (#4122)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - The REST API is the control-plane boundary for companies, agents,
plugins, adapters, costs, invites, and issue mutations.
> - Several routes still relied on broad board or company access checks
without consistently enforcing the narrower actor, company, and
active-checkout boundaries those operations require.
> - That can allow agents or non-admin users to mutate sensitive
resources outside the intended governance path.
> - This pull request hardens the route authorization layer and adds
regression coverage for the audited API surfaces.
> - The benefit is tighter multi-company isolation, safer plugin and
adapter administration, and stronger enforcement of active issue
ownership.

## What Changed

- Added route-level authorization checks for budgets, plugin
administration/scoped routes, adapter management, company import/export,
direct agent creation, invite test resolution, and issue mutation/write
surfaces.
- Enforced active checkout ownership for agent-authenticated issue
mutations, while preserving explicit management overrides for permitted
managers.
- Restricted sensitive adapter and plugin management operations to
instance-admin or properly scoped actors.
- Tightened company portability and invite probing routes so agents
cannot cross company boundaries.
- Updated access constants and the Company Access UI copy for the new
active-checkout management grant.
- Added focused regression tests covering cross-company denial, agent
self-mutation denial, admin-only operations, and active checkout
ownership.
- Rebased the branch onto `public-gh/master` and fixed validation
fallout from the rebase: heartbeat-context route ordering and a company
import/export e2e fixture that now opts out of direct-hire approval
before using direct agent creation.
- Updated onboarding and signoff e2e setup to create seed agents through
`/agent-hires` plus board approval, so they remain compatible with the
approval-gated new-agent default.
- Addressed Greptile feedback by removing a duplicate company export API
alias, avoiding N+1 reporting-chain lookups in active-checkout override
checks, allowing agent mutations on unassigned `in_progress` issues, and
blocking NAT64 invite-probe targets.

## Verification

- `pnpm exec vitest run
server/src/__tests__/issues-goal-context-routes.test.ts
cli/src/__tests__/company-import-export-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/adapter-routes-authz.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
server/src/__tests__/company-portability-routes.test.ts
server/src/__tests__/costs-service.test.ts
server/src/__tests__/invite-test-resolution-route.test.ts
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
server/src/__tests__/agent-adapter-validation-routes.test.ts`
- `pnpm exec vitest run
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts`
- `pnpm exec vitest run
server/src/__tests__/invite-test-resolution-route.test.ts`
- `pnpm -r typecheck`
- `pnpm --filter server typecheck`
- `pnpm --filter ui typecheck`
- `pnpm build`
- `pnpm test:e2e -- tests/e2e/onboarding.spec.ts
tests/e2e/signoff-policy.spec.ts`
- `pnpm test:e2e -- tests/e2e/signoff-policy.spec.ts`
- `pnpm test:run` was also run. It failed under default full-suite
parallelism with two order-dependent failures in
`plugin-routes-authz.test.ts` and `routines-e2e.test.ts`; both files
passed when rerun directly together with `pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/routines-e2e.test.ts`.

## Risks

- Medium risk: this changes authorization behavior across multiple
sensitive API surfaces, so callers that depended on broad board/company
access may now receive `403` or `409` until they use the correct
governance path.
- Direct agent creation now respects the company-level board-approval
requirement; integrations that need pending hires should use
`/api/companies/:companyId/agent-hires`.
- Active in-progress issue mutations now require checkout ownership or
an explicit management override, which may reveal workflow assumptions
in older automation.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex, GPT-5 coding agent, tool-using workflow with local shell,
Git, GitHub CLI, and repository tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 10:56:48 -05:00
Dotta
549ef11c14 [codex] Respect manual workspace runtime controls (#4125)
## Thinking Path

> - Paperclip orchestrates AI agents inside execution and project
workspaces
> - Workspace runtime services can be controlled manually by operators
and reused by agent runs
> - Manual start/stop state was not preserved consistently across
workspace policies and routine launches
> - Routine launches also needed branch/workspace variables to default
from the selected workspace context
> - This pull request makes runtime policy state explicit, preserves
manual control, and auto-fills routine branch variables from workspace
data
> - The benefit is less surprising workspace service behavior and fewer
manual inputs when running workspace-scoped routines

## What Changed

- Added runtime-state handling for manual workspace control across
execution and project workspace validators, routes, and services.
- Updated heartbeat/runtime startup behavior so manually stopped
services are respected.
- Auto-filled routine workspace branch variables from available
workspace context.
- Added focused server and UI tests for workspace runtime and routine
variable behavior.
- Removed muted gray background styling from workspace pages and cards
for a cleaner workspace UI.

## Verification

- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm exec vitest run server/src/__tests__/routines-service.test.ts
server/src/__tests__/workspace-runtime.test.ts
ui/src/components/RoutineRunVariablesDialog.test.tsx`
- Result: 55 tests passed, 21 skipped. The embedded Postgres routines
tests skipped on this host with the existing PGlite/Postgres init
warning; workspace-runtime and UI tests passed.

## Risks

- Medium risk: this touches runtime service start/stop policy and
heartbeat launch behavior.
- The focused tests cover manual runtime state, routine variables, and
workspace runtime reuse paths.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex coding agent based on GPT-5, tool-enabled local shell and
GitHub workflow, exact runtime context window not exposed in this
session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots, or documented why targeted component/service verification
is sufficient here
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 10:39:37 -05:00
Dotta
c7c1ca0c78 [codex] Clean up terminal-result adapter process groups (#4129)
## Thinking Path

> - Paperclip runs local adapter processes for agents and streams their
output into heartbeat runs
> - Some adapters can emit a terminal result before all descendant
processes have exited
> - If those descendants keep running, a heartbeat can appear complete
while the process group remains alive
> - Claude local runs need a bounded cleanup path after terminal JSON
output is observed and the child exits
> - This pull request adds terminal-result cleanup support to adapter
process utilities and wires it into the Claude local adapter
> - The benefit is fewer stranded adapter process groups after
successful terminal results

## What Changed

- Added terminal-result cleanup options to `runChildProcess`.
- Tracked child exit plus terminal output before signaling lingering
process groups.
- Added Claude local adapter configuration for terminal result cleanup
grace time.
- Added process cleanup tests covering terminal-output cleanup and noisy
non-terminal runs.

## Verification

- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts`
- Result: 9 tests passed.

## Risks

- Medium risk: this changes adapter child-process cleanup behavior.
- The cleanup only arms after terminal result detection and child exit,
and it is covered by process-group tests.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex coding agent based on GPT-5, tool-enabled local shell and
GitHub workflow, exact runtime context window not exposed in this
session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots, or documented why it is not applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 10:38:57 -05:00
Dotta
56b3120971 [codex] Improve mobile org chart navigation (#4127)
## Thinking Path

> - Paperclip models companies as teams of human and AI operators
> - The org chart is the primary visual map of that company structure
> - Mobile users need to pan and inspect the chart without awkward
gestures or layout jumps
> - The roadmap also needed to reflect that the multiple-human-users
work is complete
> - This pull request improves mobile org chart gestures and updates the
roadmap references
> - The benefit is a smoother company navigation experience and docs
that match shipped multi-user support

## What Changed

- Added one-finger mobile pan handling for the org chart.
- Expanded org chart test coverage for touch gesture behavior.
- Updated README, ROADMAP, and CLI README references to mark
multiple-human-users work as complete.

## Verification

- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm exec vitest run ui/src/pages/OrgChart.test.tsx`
- Result: 4 tests passed.

## Risks

- Low-medium risk: org chart pointer/touch handling changed, but the
behavior is scoped to the org chart page and covered by targeted tests.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex coding agent based on GPT-5, tool-enabled local shell and
GitHub workflow, exact runtime context window not exposed in this
session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots, or documented why targeted interaction tests are sufficient
here
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 10:35:33 -05:00
Dotta
4357a3f352 [codex] Harden dashboard run activity charts (#4126)
## Thinking Path

> - Paperclip gives operators a live view of agent work across
dashboards, transcripts, and run activity charts
> - Those views consume live run updates and aggregate run activity from
backend dashboard data
> - Missing or partial run data could make charts brittle, and live
transcript updates were heavier than needed
> - Operators need dashboard data to stay stable even when recent run
payloads are incomplete
> - This pull request hardens dashboard run aggregation, guards chart
rendering, and lightens live run update handling
> - The benefit is a more reliable dashboard during active agent
execution

## What Changed

- Added dashboard run activity types and backend aggregation coverage.
- Guarded activity chart rendering when run data is missing or partial.
- Reduced live transcript update churn in active agent and run chat
surfaces.
- Fixed issue chat avatar alignment in the thread renderer.
- Added focused dashboard, activity chart, and live transcript tests.

## Verification

- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm exec vitest run server/src/__tests__/dashboard-service.test.ts
ui/src/components/ActivityCharts.test.tsx
ui/src/components/transcript/useLiveRunTranscripts.test.tsx`
- Result: 8 tests passed, 1 skipped. The embedded Postgres dashboard
service test skipped on this host with the existing PGlite/Postgres init
warning; UI chart and transcript tests passed.

## Risks

- Medium-low risk: aggregation semantics changed, but the UI remains
guarded around incomplete data.
- The dashboard service test is host-skipped here, so CI should confirm
the embedded database path.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex coding agent based on GPT-5, tool-enabled local shell and
GitHub workflow, exact runtime context window not exposed in this
session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots, or documented why targeted component tests are sufficient
here
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 10:34:21 -05:00
Dotta
0f4e4b4c10 [codex] Split reusable agent hiring templates (#4124)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Hiring new agents depends on clear, reusable operating instructions
> - The create-agent skill had one large template reference that mixed
multiple roles together
> - That made it harder to reuse, review, and adapt role-specific
instructions during governed hires
> - This pull request splits the reusable agent instruction templates
into focused role files and polishes the agent instructions pane layout
> - The benefit is faster, clearer agent hiring without bloating the
main skill document

## What Changed

- Split coder, QA, and UX designer reusable instructions into dedicated
reference files.
- Kept the index reference concise and pointed it at the role-specific
files.
- Updated the create-agent skill to describe the separated template
structure.
- Polished the agent detail instructions/package file tree layout so the
longer template references remain readable.

## Verification

- `pnpm install --frozen-lockfile --ignore-scripts`
- `pnpm --filter @paperclipai/ui typecheck`
- UI screenshot rationale: no screenshots attached because the visible
change is limited to the Agent detail instructions file-tree layout
(`wrapLabels` plus the side-by-side breakpoint). There is no new user
flow or state transition to demonstrate; reviewers can verify visually
by opening an agent's Instructions tab and resizing across the
single-column and side-by-side breakpoints to confirm long file names
wrap instead of truncating or overflowing.

## Risks

- Low risk: this is documentation and UI layout only.
- Main risk is stale links in the skill references; the new files are
committed in the referenced paths.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex coding agent based on GPT-5, tool-enabled local shell and
GitHub workflow, exact runtime context window not exposed in this
session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots, or documented why targeted component/type verification is
sufficient here
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 10:33:19 -05:00
Aron Prins
73eb23734f docs: use structured agent mentions in paperclip skill (#4103)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Agents coordinate work through tasks and comments, and @-mentions
are part of the wakeup path for cross-agent handoffs and review requests
> - The current repo skill still instructs machine-authored comments to
use raw `@AgentName` text as the default mention format
> - But the current backend mention parsing is still unreliable for
multi-word display names, so agents following that guidance can silently
fail to wake the intended target
> - This pull request updates the Paperclip skill and API reference to
prefer structured `agent://` markdown mentions for machine-authored
comments
> - The benefit is a low-risk documentation workaround that steers
agents onto the mention format the server already resolves reliably
while broader runtime fixes are reviewed upstream

## What Changed

- Updated `skills/paperclip/SKILL.md` to stop recommending raw
`@AgentName` mentions for machine-authored comments
- Updated `skills/paperclip/references/api-reference.md` with a concrete
workflow: resolve the target via `GET
/api/companies/{companyId}/agents`, then emit `[@Display
Name](agent://<agent-id>)`
- Added explicit guidance that raw `@AgentName` text is fallback-only
and unreliable for names containing spaces
- Cross-referenced the current upstream mention-bug context so reviewers
can connect this docs workaround to the open parser/runtime fixes
  Related issue/PR refs: #448, #459, #558, #669, #722, #1412, #2249

## Verification

- `pnpm -r typecheck`
- `pnpm build`
- `pnpm test:run` currently fails on upstream `master` in existing tests
unrelated to this docs-only change:
- `src/__tests__/worktree.test.ts` — `seeds authenticated users into
minimally cloned worktree instances` timed out after 20000ms
- `src/__tests__/onboard.test.ts` — `keeps tailnet quickstart on
loopback until tailscale is available` expected `127.0.0.1` but got
`100.125.202.3`
- Confirmed the git diff is limited to:
  - `skills/paperclip/SKILL.md`
  - `skills/paperclip/references/api-reference.md`

## Risks

- Low risk. This is a docs/skill-only change and does not alter runtime
behavior.
- It is a mitigation, not a full fix: it helps agent-authored comments
that follow the Paperclip skill, but it does not fix manually typed raw
mentions or other code paths that still emit plain `@Name` text.
- If upstream chooses a different long-term mention format, this
guidance may need to be revised once the runtime-side fix lands.

## Model Used

- OpenAI Codex desktop agent on a GPT-5-class model. Exact deployed
model ID and context window are not exposed by the local harness. Tool
use enabled, including shell execution, git, and GitHub CLI.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-20 07:38:04 -07:00
Dotta
9c6f551595 [codex] Add plugin orchestration host APIs (#4114)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - The plugin system is the extension path for optional capabilities
that should not require core product changes for every integration.
> - Plugins need scoped host APIs for issue orchestration, documents,
wakeups, summaries, activity attribution, and isolated database state.
> - Without those host APIs, richer plugins either cannot coordinate
Paperclip work safely or need privileged core-side special cases.
> - This pull request adds the plugin orchestration host surface, scoped
route dispatch, a database namespace layer, and a smoke plugin that
exercises the contract.
> - The benefit is a broader plugin API that remains company-scoped,
auditable, and covered by tests.

## What Changed

- Added plugin orchestration host APIs for issue creation, document
access, wakeups, summaries, plugin-origin activity, and scoped API route
dispatch.
- Added plugin database namespace tables, schema exports, migration
checks, and idempotent replay coverage under migration
`0059_plugin_database_namespaces`.
- Added shared plugin route/API types and validators used by server and
SDK boundaries.
- Expanded plugin SDK types, protocol helpers, worker RPC host behavior,
and testing utilities for orchestration flows.
- Added the `plugin-orchestration-smoke-example` package to exercise
scoped routes, restricted database namespaces, issue orchestration,
documents, wakeups, summaries, and UI status surfaces.
- Kept the new orchestration smoke fixture out of the root pnpm
workspace importer so this PR preserves the repository policy of not
committing `pnpm-lock.yaml`.
- Updated plugin docs and database docs for the new orchestration and
database namespace surfaces.
- Rebased the branch onto `public-gh/master`, resolved conflicts, and
removed `pnpm-lock.yaml` from the final PR diff.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm exec vitest run packages/db/src/client.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-database.test.ts
server/src/__tests__/plugin-orchestration-apis.test.ts
server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/plugin-scoped-api-routes.test.ts
server/src/__tests__/plugin-sdk-orchestration-contract.test.ts`
- From `packages/plugins/examples/plugin-orchestration-smoke-example`:
`pnpm exec vitest run --config ./vitest.config.ts`
- `pnpm --dir
packages/plugins/examples/plugin-orchestration-smoke-example run
typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- PR CI on latest head `293fc67c`: `policy`, `verify`, `e2e`, and
`security/snyk` all passed.

## Risks

- Medium risk: this expands plugin host authority, so route auth,
company scoping, and plugin-origin activity attribution need careful
review.
- Medium risk: database namespace migration behavior must remain
idempotent for environments that may have seen earlier branch versions.
- Medium risk: the orchestration smoke fixture is intentionally excluded
from the root workspace importer to avoid a `pnpm-lock.yaml` PR diff;
direct fixture verification remains listed above.
- Low operational risk from the PR setup itself: the branch is rebased
onto current `master`, the migration is ordered after upstream
`0057`/`0058`, and `pnpm-lock.yaml` is not in the final diff.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

Roadmap checked: this work aligns with the completed Plugin system
milestone and extends the plugin surface rather than duplicating an
unrelated planned core feature.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in a tool-enabled CLI
environment. Exact hosted model build and context-window size are not
exposed by the runtime; reasoning/tool use were enabled for repository
inspection, editing, testing, git operations, and PR creation.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (N/A: no core UI screen change; example plugin UI contract
is covered by tests)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 08:52:51 -05:00
Dotta
16b2b84d84 [codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.

## What Changed

- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.

## Risks

- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:19:48 -05:00
Dotta
057fee4836 [codex] Polish issue and operator workflow UI (#4090)
## Thinking Path

> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.

## What Changed

- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.

## Risks

- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
Dotta
fee514efcb [codex] Improve workspace navigation and runtime UI (#4089)
## Thinking Path

> - Paperclip agents do real work in project and execution workspaces.
> - Operators need workspace state to be visible, navigable, and
copyable without digging through raw run logs.
> - The branch included related workspace cards, navigation, runtime
controls, stale-service handling, and issue-property visibility.
> - These changes share the workspace UI and runtime-control surfaces
and can stand alone from unrelated access/profile work.
> - This pull request groups the workspace experience changes into one
standalone branch.
> - The benefit is a clearer workspace overview, better metadata copy
flows, and more accurate runtime service controls.

## What Changed

- Polished project workspace summary cards and made workspace metadata
copyable.
- Added a workspace navigation overview and extracted reusable project
workspace content.
- Squared and polished the execution workspace configuration page.
- Fixed stale workspace command matching and hid stopped stale services
in runtime controls.
- Showed live workspace service context in issue properties.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
ui/src/components/ProjectWorkspaceSummaryCard.test.tsx
ui/src/lib/project-workspaces-tab.test.ts
ui/src/components/Sidebar.test.tsx
ui/src/components/WorkspaceRuntimeControls.test.tsx
ui/src/components/IssueProperties.test.tsx`
- `pnpm exec vitest run packages/shared/src/workspace-commands.test.ts
--config /dev/null` because the root Vitest project config does not
currently include `packages/shared` tests.
- Split integration check: merged after runtime/governance,
dev-infra/backups, and access/profiles with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.

## Risks

- Medium risk: touches workspace navigation, runtime controls, and issue
property rendering.
- Visual layout changes may need browser QA, especially around smaller
screens and dense workspace metadata.
- No database migrations are included.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:14:32 -05:00
Dotta
d8b63a18e7 [codex] Add access cleanup and user profile page (#4088)
## Thinking Path

> - Paperclip is moving from a solo local operator model toward teams
supervising AI-agent companies.
> - Human access management and human-visible profile surfaces are part
of that multiple-user path.
> - The branch included related access cleanup, archived-member removal,
permission protection, and a user profile page.
> - These changes share company membership, user attribution, and
access-service behavior.
> - This pull request groups those human access/profile changes into one
standalone branch.
> - The benefit is safer member removal behavior and a first profile
surface for user work, activity, and cost attribution.

## What Changed

- Added archived company member removal support across shared contracts,
server routes/services, and UI.
- Protected company member removal with stricter permission checks and
tests.
- Added company user profile API, shared types, route wiring, client
API, route, and UI page.
- Simplified the user profile page visual design to a neutral
typography-led layout.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/access-service.test.ts
server/src/__tests__/user-profile-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx --hookTimeout=30000`
- `pnpm exec vitest run server/src/__tests__/user-profile-routes.test.ts
--testTimeout=30000 --hookTimeout=30000` after an initial local
embedded-Postgres hook timeout in the combined run.
- Split integration check: merged after runtime/governance and
dev-infra/backups with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.

## Risks

- Medium risk: changes member removal permissions and adds a new user
profile route with cross-table stats.
- The profile page is a new UI surface and may need visual follow-up in
browser QA.
- No database migrations are included.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:10:20 -05:00
Dotta
e89d3f7e11 [codex] Add backup endpoint and dev runtime hardening (#4087)
## Thinking Path

> - Paperclip is a local-first control plane for AI-agent companies.
> - Operators need predictable local dev behavior, recoverable instance
data, and scripts that do not churn the running app.
> - Several accumulated changes improve backup streaming, dev-server
health, static UI caching/logging, diagnostic-file ignores, and instance
isolation.
> - These are operational improvements that can land independently from
product UI work.
> - This pull request groups the dev-infra and backup changes from the
split branch into one standalone branch.
> - The benefit is safer local operation, easier manual backups, less
noisy dev output, and less cross-instance auth leakage.

## What Changed

- Added a manual instance database backup endpoint and route tests.
- Streamed backup/restore handling to avoid materializing large payloads
at once.
- Reduced dev static UI log/cache churn and ignored Node diagnostic
report captures.
- Added guarded dev auto-restart health polling coverage.
- Preserved worktree config during provisioning and scoped auth cookies
by instance.
- Added a Discord daily digest helper script and environment
documentation.
- Hardened adapter-route and startup feedback export tests around the
changed infrastructure.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run packages/db/src/backup-lib.test.ts
server/src/__tests__/instance-database-backups-routes.test.ts
server/src/__tests__/server-startup-feedback-export.test.ts
server/src/__tests__/adapter-routes.test.ts
server/src/__tests__/dev-runner-paths.test.ts
server/src/__tests__/health-dev-server-token.test.ts
server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/vite-html-renderer.test.ts
server/src/__tests__/workspace-runtime.test.ts
server/src/__tests__/better-auth.test.ts`
- Split integration check: merged after the runtime/governance branch
and before UI branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.

## Risks

- Medium risk: touches server startup, backup streaming, auth cookie
naming, dev health checks, and worktree provisioning.
- Backup endpoint behavior depends on existing board/admin access
controls and database backup helpers.
- No database migrations are included.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:08:55 -05:00
Dotta
236d11d36f [codex] Add run liveness continuations (#4083)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.

## What Changed

- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.

## Verification

- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.

## Risks

- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
Dotta
b9a80dcf22 feat: implement multi-user access and invite flows (#3784)
## Thinking Path

> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.

## What Changed

- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.

## Verification

- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.

## Risks

- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.

## Model Used

- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.

---------

Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
Roman Barinov
e93e418cbf fix: add ssh client and jq to production image (#3826)
## Thinking Path

> - Paperclip is the control plane that runs long-lived AI-agent work in
production.
> - The production container image is the runtime boundary for agent
tools and shell access.
> - In our deployment, Paperclip agents now need a native SSH client and
`jq` available inside the final runtime container.
> - Installing those tools only via ai-rig entrypoint hacks is brittle
and drifts from the image source of truth.
> - This pull request updates the production Docker image itself so the
required binaries are present whenever the image is built.
> - The change is intentionally scoped to the final production stage so
build/deps stages do not gain extra packages unnecessarily.
> - The benefit is a cleaner, reproducible runtime image with fewer
deploy-specific workarounds.

## What Changed

- Added `openssh-client` to the production Docker image stage.
- Added `jq` to the production Docker image stage.
- Kept the package install in the final `production` stage instead of
the shared base stage to minimize scope.

## Verification

- Reviewed the final Dockerfile diff to confirm the packages are
installed in the `production` stage only.
- Attempted local image build with:
  - `docker build --target production -t paperclip:ssh-jq-test .`
- Local build could not be completed in this environment because the
local Docker daemon was unavailable:
- `Cannot connect to the Docker daemon at
unix:///Users/roman/.docker/run/docker.sock. Is the docker daemon
running?`

## Risks

- Low risk: image footprint increases slightly because two Debian
packages are added.
- `openssh-client` expands runtime capability, so this is appropriate
only because the deployed Paperclip runtime explicitly needs SSH access.

## Model Used

- OpenAI Codex / `gpt-5.4`
- Tool-using agent workflow via Hermes
- Context from local repository inspection, git, and shell tooling

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-16 17:11:55 -05:00
Dotta
407e76c1db [codex] Fix Docker gh installation (#3844)
## Thinking Path

> - Paperclip is the control plane for autonomous AI companies, and the
Docker image is the no-local-Node path for running that control plane.
> - The deploy workflow builds and pushes that image from the repository
`Dockerfile`.
> - The current image setup adds GitHub CLI through GitHub's external
apt repository and verifies a mutable keyring URL with a pinned SHA256.
> - GitHub rotated the CLI Linux package signing key, so that pinned
keyring checksum now fails before Buildx can publish the image.
> - Paperclip already has a repo-local precedent in
`docker/untrusted-review/Dockerfile`: install Debian trixie's packaged
`gh` directly from the base distribution.
> - This pull request removes the external GitHub CLI apt
keyring/repository path from the production image and installs `gh` with
the rest of the Debian packages.
> - The benefit is a simpler Docker build that no longer fails when
GitHub rotates the apt keyring file.

## What Changed

- Updated the main `Dockerfile` base stage to install `gh` from Debian
trixie's package repositories.
- Removed the mutable GitHub CLI apt keyring download, pinned checksum
verification, extra apt source, second `apt-get update`, and separate
`gh` install step.

## Verification

- `git diff --check`
- `./scripts/docker-build-test.sh` skipped because Docker is installed
but the daemon is not running on this machine.
- Confirmed `https://packages.debian.org/trixie/gh` returns HTTP 200,
matching the base image distribution package source.

## Risks

- Debian's `gh` package can lag the latest upstream GitHub CLI release.
This is acceptable for the current image contract, which requires `gh`
availability but does not document a latest-upstream version guarantee.
- A full image build still needs to run in CI because the local Docker
daemon is unavailable in this environment.

## Model Used

- OpenAI Codex, GPT-5-based coding agent. Exact backend model ID was not
exposed in this runtime; tool use and shell execution were enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-16 17:10:42 -05:00
Devin Foley
e458145583 docs: add public roadmap and update contribution policy for feature PRs (#3835)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - As the project grows, more contributors want to build features —
which is great
> - Without a public roadmap or clear contribution guidance,
contributors spend time on PRs that overlap with planned core work
> - This creates frustration on both sides when those PRs can't be
merged
> - This PR publishes a roadmap, updates the contribution guide with a
clear path for feature proposals, and reinforces the workflow in the PR
template
> - The benefit is that contributors know exactly how to propose
features and where to focus for the highest-impact contributions

## What Changed

- Added `ROADMAP.md` with expanded descriptions of all shipped and
planned milestones, plus guidance on coordinating feature contributions
- Added "Feature Contributions" section to `CONTRIBUTING.md` explaining
how to propose features (check roadmap → discuss in #dev → consider the
plugin system)
- Updated `.github/PULL_REQUEST_TEMPLATE.md` with a callout linking to
the roadmap and a new checklist item to check for overlap with planned
work, while preserving the newer required `Model Used` section from
`master`
- Added `Memory / Knowledge` to the README roadmap preview and linked
the preview to the full `ROADMAP.md`

## Verification

- Open `ROADMAP.md` on GitHub and confirm it renders correctly with all
milestone sections
- Read the new "Feature Contributions" section in `CONTRIBUTING.md` and
verify all links resolve
- Open a new PR and confirm the template shows the roadmap callout and
the new checklist item
- Verify README links to `ROADMAP.md` and the roadmap preview includes
"Memory / Knowledge"

## Risks

- Docs-only change — no runtime or behavioral impact
- Contribution policy changes were written to be constructive and to
offer clear alternative paths (plugins, coordination via #dev, reference
implementations as feedback)

## Model Used

- OpenAI Codex local agent (GPT-5-based coding model; exact runtime
model ID is not exposed in this environment)
- Tool use enabled for shell, git, GitHub CLI, and patch application
- Used to rebase the branch, resolve merge conflicts, update the PR
metadata, and verify the repo state

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable (N/A — docs only)
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — no UI changes)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-16 13:04:50 -07:00
Dewaldt Huysamen
f701c3e78c feat(claude-local): add Opus 4.7 to adapter model dropdown (#3828)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Each adapter advertises a model list that powers the agent config UI
dropdown
> - The `claude_local` adapter's dropdown is sourced from the hard-coded
`models` array in `packages/adapters/claude-local/src/index.ts`
> - Anthropic recently released Opus 4.7, the newest current-generation
Opus model
> - Without a list entry, users cannot discover or select Opus 4.7 from
the dropdown (they can still type it manually, since the field is
creatable, but discoverability is poor)
> - This pull request adds `claude-opus-4-7` to the `claude_local` model
list so new agents can be configured with the latest model by default
> - The benefit is out-of-the-box access to the newest Opus model,
consistent with how every other current-generation Claude model is
already listed

## What Changed

- Added `{ id: "claude-opus-4-7", label: "Claude Opus 4.7" }` as the
**first** entry of the `models` array in
`packages/adapters/claude-local/src/index.ts`. Newest-first ordering
matches the convention already used for 4.6.

## Verification

- `pnpm --filter @paperclipai/adapter-claude-local typecheck` → passes.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/adapter-models.test.ts
src/__tests__/claude-local-adapter.test.ts` → 12/12 passing (both
directly-related files).
- No existing test pins the `claude_local` models array (see
`server/src/__tests__/adapter-models.test.ts`), so appending a new entry
is non-breaking.
- Manual check of UI consumer: `AgentConfigForm.tsx` fetches the list
via `agentsApi.adapterModels()` and renders it in a creatable popover —
no hard-coded expectations anywhere in the UI layer.
- Screenshots: single new option appears at the top of the Claude Code
(local) model dropdown; existing options unchanged.

## Risks

- Low risk. Purely additive: one new entry in a list consumed by a UI
dropdown. No behavior change for existing agents, no schema change, no
migration, no env var.
- `BEDROCK_MODELS` in
`packages/adapters/claude-local/src/server/models.ts` is intentionally
**not** touched — the exact region-qualified Bedrock id for Opus 4.7 is
not yet confirmed, and shipping a guessed id could produce a broken
option for Bedrock users. Tracked as a follow-up on the linked issue.

## Model Used

- None — human-authored.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable (no tests needed:
existing suite already covers the list-consumer paths)
- [x] If this change affects the UI, I have included before/after
screenshots (dropdown gains one new top entry; all other entries
unchanged)
- [x] I have updated relevant documentation to reflect my changes (no
doc update needed: `docs/adapters/claude-local.md` uses
`claude-opus-4-6` only as an example, still valid)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Closes #3827
2026-04-16 13:18:30 -05:00
akhater
1afb6be961 fix(heartbeat): add hermes_local to SESSIONED_LOCAL_ADAPTERS (#3561)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The heartbeat service monitors agent health via PID liveness checks
for local adapters
> - `SESSIONED_LOCAL_ADAPTERS` in `heartbeat.ts` controls which adapters
get PID tracking and retry-on-lost behavior
> - `hermes_local` (the Hermes Agent adapter) was missing from this set
> - Without it, the orphan reaper immediately marks all Hermes runs as
`process_lost` instead of retrying
> - This PR adds the one-line registration so `hermes_local` gets the
same treatment as `claude_local`, `codex_local`, `cursor`, and
`gemini_local`
> - The benefit is Hermes agent runs complete normally instead of being
killed after ~5 minutes

## What Changed

- Added `"hermes_local"` to the `SESSIONED_LOCAL_ADAPTERS` set in
`server/src/services/heartbeat.ts`

## Verification

- Trigger a Hermes agent run via the wakeup API
- Confirm `heartbeat_runs.status` transitions to `succeeded` (not
`process_lost`)
- Tested end-to-end on a production Paperclip instance with Hermes agent
running heartbeat cycles for 48+ hours

## Risks

Low risk. Additive one-line change — adds a string to an existing set.
No behavioral change for other adapters. Consistent with
`BUILTIN_ADAPTER_TYPES` which already includes `hermes_local`.

## Model Used

- Provider: Anthropic
- Model: Claude Opus 4.6 (claude-opus-4-6)
- Context window: 1M tokens
- Capabilities: Tool use, code execution

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Antoine Khater <akhater@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 07:35:02 -05:00
Dotta
b8725c52ef release: v2026.416.0 notes (#3782)
## Thinking Path

> - Paperclip is the control plane for autonomous AI companies, and
stable releases need a clear changelog artifact for operators upgrading
between versions.
> - The release-note workflow in this repo stores one stable changelog
file per release under `releases/`.
> - `v2026.410.0` and `v2026.413.0` were intermediate drafts for the
same release window, while the next stable release is `v2026.416.0`.
> - Keeping superseded draft release notes around would make the stable
release history noisy and misleading.
> - This pull request consolidates the intended content into
`releases/v2026.416.0.md` and removes the older
`releases/v2026.410.0.md` and `releases/v2026.413.0.md` files.
> - The benefit is a single canonical stable release note for
`v2026.416.0` with no duplicate release artifacts.

## What Changed

- Added `releases/v2026.416.0.md` as the canonical stable changelog for
the April 16, 2026 release.
- Removed the superseded `releases/v2026.410.0.md` and
`releases/v2026.413.0.md` draft release-note files.
- Kept the final release-note ordering and content as edited in the
working tree before commit.

## Verification

- Reviewed the git diff to confirm the PR only changes release-note
artifacts in `releases/`.
- Confirmed the branch is based on `public-gh/master` and contains a
single release-note commit.
- Did not run tests because this is a docs-only changelog update.

## Risks

- Low risk. The change is limited to release-note markdown files.
- The main risk is editorial: if any release item was meant to stay in a
separate changelog file, it now exists only in `v2026.416.0.md`.

## Model Used

- OpenAI GPT-5 Codex, model `gpt-5.4`, medium reasoning, tool use and
code execution in the Codex CLI environment.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-15 21:40:35 -05:00
Dotta
5f45712846 Sync/master post pap1497 followups 2026 04 15 (#3779)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The board depends on issue, inbox, cost, and company-skill surfaces
to stay accurate and fast while agents are actively working
> - The PAP-1497 follow-up branch exposed a few rough edges in those
surfaces: stale active-run state on completed issues, missing creator
filters, oversized issue payload scans, and placeholder issue-route
parsing
> - Those gaps make the control plane harder to trust because operators
can see misleading run state, miss the right subset of work, or pay
extra query/render cost on large issue records
> - This pull request tightens those follow-ups across server and UI
code, and adds regression coverage for the affected paths
> - The benefit is a more reliable issue workflow, safer high-volume
cost aggregation, and clearer board/operator navigation

## What Changed

- Added the `v2026.415.0` release changelog entry.
- Fixed stale issue-run presentation after completion and reused the
shared issue-path parser so literal route placeholders no longer become
issue links.
- Added creator filters to the Issues page and Inbox, including
persisted filter-state normalization and regression coverage.
- Bounded issue detail/list project-mention scans and trimmed large
issue-list payload fields to keep issue reads lighter.
- Hardened company-skill list projection and cost/finance aggregation so
large markdown blobs and large summed values do not leak into list
responses or overflow 32-bit casts.
- Added targeted server/UI regression tests for company skills,
costs/finance, issue mention scanning, creator filters, inbox
normalization, and issue reference parsing.

## Verification

- `pnpm exec vitest run
server/src/__tests__/company-skills-service.test.ts
server/src/__tests__/costs-service.test.ts
server/src/__tests__/issues-goal-context-routes.test.ts
server/src/__tests__/issues-service.test.ts ui/src/lib/inbox.test.ts
ui/src/lib/issue-filters.test.ts ui/src/lib/issue-reference.test.ts`
- `gh pr checks 3779`
Current pass set on the PR head: `policy`, `verify`, `e2e`,
`security/snyk (cryppadotta)`, `Greptile Review`

## Risks

- Creator filter options are derived from the currently loaded
issue/agent data, so very sparse result sets may not surface every
historical creator until they appear in the active dataset.
- Cost/finance aggregate casts now use `double precision`; that removes
the current overflow risk, but future schema changes should keep
large-value aggregation behavior under review.
- Issue detail mention scanning now skips comment-body scans on the
detail route, so any consumer that relied on comment-only project
mentions there would need to fetch them separately.

## Model Used

- OpenAI Codex, GPT-5-based coding agent with terminal tool use and
local code execution in the Paperclip workspace. Exact internal model
ID/context-window exposure is not surfaced in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 21:13:56 -05:00
Dotta
d4c3899ca4 [codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Operators rely on issue, inbox, and routine views to understand what
the company is doing in real time
> - Those views need to stay fast and readable even when issue lists,
markdown comments, and run metadata get large
> - The current branch had a coherent set of UI and live-update
improvements spread across issue search, issue detail rendering, routine
affordances, and workspace lookups
> - This pull request groups those board-facing changes into one
standalone branch that can merge independently of the heartbeat/runtime
work
> - The benefit is a faster, clearer issue and routine workflow without
changing the underlying task model

## What Changed

- Show routine execution issues by default and rename the filter to
`Hide routine runs` so the default state no longer looks like an active
filter.
- Show the routine name in the run dialog and tighten the issue
properties pane with a workspace link, copy-on-click behavior, and an
inline parent arrow.
- Reduce issue detail rerenders, keep queued issue chat mounted, improve
issues page search responsiveness, and speed up issues first paint.
- Add inbox "other search results", refresh visible issue runs after
status updates, and optimize workspace lookups through summary-mode
execution workspace queries.
- Improve markdown wrapping and scrolling behavior for long strings and
self-comment code blocks.
- Relax the markdown sanitizer assertion so the test still validates
safety after the new wrap-friendly inline styles.

## Verification

- `pnpm vitest run ui/src/components/IssuesList.test.tsx
ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx
ui/src/context/BreadcrumbContext.test.tsx
ui/src/context/LiveUpdatesProvider.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/api/execution-workspaces.test.ts
server/src/__tests__/execution-workspaces-routes.test.ts`

## Risks

- This touches several issue-facing UI surfaces at once, so regressions
would most likely show up as stale rendering, search result mismatches,
or small markdown presentation differences.
- The workspace lookup optimization depends on the summary-mode route
shape staying aligned between server and UI.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 15:54:05 -05:00
Jannes Stubbemann
7463479fc8 fix: disable HTTP caching on run log endpoints (#3724)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Every run emits a streaming log that the web UI polls so humans can
watch what the agent is doing
> - Log responses go out without explicit cache directives, so Express
adds an ETag
> - If the first poll lands before any bytes have been written, the
browser caches the empty / partial snapshot and keeps getting `304 Not
Modified` on every subsequent poll
> - The transcript pane then stays stuck on "Waiting for transcript…"
even after the log has plenty of content
> - This pull request sets `Cache-Control: no-cache, no-store` on both
run-log endpoints so the conditional-request path is defeated

## What Changed

- `server/src/routes/agents.ts` — `GET /heartbeat-runs/:runId/log` now
sets `Cache-Control: no-cache, no-store` on the response.
- Same change applied to `GET /workspace-operations/:operationId/log`
(same structure, same bug).

## Verification

- Reproduction: start a long-running agent, watch the transcript pane.
Before the fix, open devtools and observe `304 Not Modified` on each
poll after the initial 200 with an empty body; the UI never updates.
After the fix, each poll is a 200 with fresh bytes.
- Existing tests pass.

## Risks

Low. Cache headers only affect whether the browser revalidates; the
response body is unchanged. No API surface change.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:53:25 -05:00
Dotta
3fa5d25de1 [codex] harden heartbeat run summaries and recovery context (#3742)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Heartbeat runs are the control-plane record of what agents did, why
they woke up, and what operators should see next
> - Run lists, stranded issue comments, and live log polling all depend
on compact but accurate heartbeat summaries
> - The current branch had a focused backend slice that improves how run
result JSON is summarized, how stale process recovery comments are
written, and how live log polling resolves the active run
> - This pull request isolates that heartbeat/runtime reliability work
from the unrelated UI and dev-tooling changes
> - The benefit is more reliable issue context and cheaper run lookups
without dragging unrelated board UI changes into the same review

## What Changed

- Include the latest run failure in stranded issue comments during
orphaned process recovery.
- Bound heartbeat `result_json` payloads for list responses while
preserving the raw stored payloads.
- Narrow heartbeat log endpoint lookups so issue polling resolves the
relevant active run with less unnecessary scanning.
- Add focused tests for heartbeat list summaries, live run polling,
orphaned process recovery, and the run context/result summary helpers.

## Verification

- `pnpm vitest run
server/src/__tests__/heartbeat-context-summary.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/agent-live-run-routes.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts`

## Risks

- The main risk is accidentally hiding a field that some client still
expects from summarized `result_json`, or over-constraining the live log
lookup path for edge-case run routing.
- Recovery comments now surface the latest failure more aggressively, so
wording changes may affect downstream expectations if anyone parses
those comments too strictly.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-15 09:48:39 -05:00
Dotta
c1a02497b0 [codex] fix worktree dev dependency ergonomics (#3743)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Local development needs to work cleanly across linked git worktrees
because Paperclip itself leans on worktree-based engineering workflows
> - Dev-mode asset routing, Vite watch behavior, and workspace package
links are part of that day-to-day control-plane ergonomics
> - The current branch had a small but coherent set of
worktree/dev-tooling fixes that are independent from both the issue UI
changes and the heartbeat runtime changes
> - This pull request isolates those environment fixes into a standalone
branch that can merge without carrying unrelated product work
> - The benefit is a smoother multi-worktree developer loop with fewer
stale links and less noisy dev watching

## What Changed

- Serve dev public assets before the HTML shell and add a routing test
that locks that behavior in.
- Ignore UI test files in the Vite dev watch helper so the dev server
does less unnecessary work.
- Update `ensure-workspace-package-links.ts` to relink stale workspace
dependencies whenever a workspace `node_modules` directory exists,
instead of only inside linked-worktree detection paths.

## Verification

- `pnpm vitest run server/src/__tests__/app-vite-dev-routing.test.ts
ui/src/lib/vite-watch.test.ts`
- `node cli/node_modules/tsx/dist/cli.mjs
scripts/ensure-workspace-package-links.ts`

## Risks

- The asset routing change is low risk but sits near app shell behavior,
so a regression would show up as broken static assets in dev mode.
- The workspace-link repair now runs in more cases, so the main risk is
doing unexpected relinks when a checkout has intentionally unusual
workspace symlink state.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment.
Exact backend model deployment ID was not exposed in-session.
Tool-assisted editing and shell execution were used.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-15 09:47:29 -05:00
Jannes Stubbemann
390502736c chore(ui): drop console.* and legal comments in production builds (#3728)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The web UI is a single-page app built with Vite and shipped as a
static bundle to every deployment
> - Production bundles carry `console.log` / `console.debug` calls from
dev code and `/*! … */` legal-comment banners from third-party packages
> - The console calls leak internals to anyone opening devtools and
waste bytes per call site; the legal banners accumulate throughout the
bundle
> - Both problems affect every self-hoster, since they all ship the same
UI bundle
> - This pull request configures esbuild (via `vite.config.ts`) to strip
`console` and `debugger` statements and drop inline legal comments from
production builds only

## What Changed

- `ui/vite.config.ts`:
  - Switch to the functional `defineConfig(({ mode }) => …)` form.
- Add `build.minify: "esbuild"` (explicit — it's the existing default).
- Add `esbuild.drop: ["console", "debugger"]` and
`esbuild.legalComments: "none"`, gated on `mode === "production"` so
`vite dev` is unaffected.

## Verification

- `pnpm --filter @paperclipai/ui build` then grep the
`ui/dist/assets/*.js` bundle for `console.log` — no occurrences.
- `pnpm --filter @paperclipai/ui dev` — `console.log` calls in source
still reach the browser console.
- Bundle size: small reduction (varies with project but measurable on a
fresh build).

## Risks

Low. No API surface change. Production code should not depend on
`console.*` for side effects; any call that did is now a dead call,
which is the same behavior most minifiers apply.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:46:12 -05:00
Jannes Stubbemann
0d87fd9a11 fix: proper cache headers for static assets and SPA fallback (#3734)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Every deployment serves the same Vite-built UI bundle from the same
express app
> - Vite emits JS/CSS under `/assets/<name>.<hash>.<ext>` — the hash
rolls whenever the content rolls, so these files are inherently
immutable
> - `index.html` references specific hashed filenames, so it has the
opposite lifecycle: whenever we deploy, the file changes but the URL
doesn't
> - Today the static middleware sends neither with cache headers, and
the SPA fallback serves `index.html` for any unmatched route — including
paths under `/assets/` that no longer exist after a deploy
> - That combination produces the familiar "blank screen after deploy" +
`Failed to load module script: Expected a JavaScript MIME type but
received 'text/html'` bug
> - This pull request caches hashed assets immutably, forces
`index.html` to `no-cache` everywhere it gets served, and returns 404
for missing `/assets/*` paths

## What Changed

- `server/src/app.ts`:
- Serve `/assets/*` with `Cache-Control: public, max-age=31536000,
immutable`.
- Serve the remaining static files (favicon, manifest, robots.txt) with
a 1-hour cache, but override to `no-cache` specifically for `index.html`
via the `setHeaders` hook — because `express.static` serves it directly
for `/` and `/index.html`.
- The SPA fallback (`app.get(/.*/, …)`) sets `Cache-Control: no-cache`
on its `index.html` response.
- The fallback returns 404 for paths under `/assets/` so browsers don't
cache the HTML shell as a JavaScript module.

## Verification

- `curl -i http://localhost:3100/assets/index-abc123.js` →
`cache-control: public, max-age=31536000, immutable`.
- `curl -i http://localhost:3100/` → `cache-control: no-cache`.
- `curl -i http://localhost:3100/assets/missing.js` → `404`.
- `curl -i http://localhost:3100/some/spa/route` → `200` HTML with
`cache-control: no-cache`.

## Risks

Low. Asset URLs and HTML content are unchanged; only response headers
and the 404 behavior for missing asset paths change. No API surface
affected.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:45:22 -05:00
Jannes Stubbemann
6059c665d5 fix(a11y): remove maximum-scale and user-scalable=no from viewport (#3726)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Humans watch and oversee those agents through a web UI
> - Accessibility matters for anyone who cannot read small text
comfortably — they rely on browser zoom
> - The app shell's viewport meta tag includes `maximum-scale=1.0,
user-scalable=no`
> - Those tokens disable pinch-zoom and are a WCAG 2.1 SC 1.4.4 (Resize
Text) failure
> - The original motivation — suppressing iOS Safari's auto-zoom on
focused inputs — is actually a font-size issue, not a viewport issue,
and modern Safari only auto-zooms when input font-size is below 16px
> - This pull request drops the two tokens, restoring pinch-zoom while
leaving the real fix (inputs at ≥16px) to CSS

## What Changed

- `ui/index.html` — remove `maximum-scale=1.0, user-scalable=no` from
the viewport meta tag. Keep `width=device-width, initial-scale=1.0,
viewport-fit=cover`.

## Verification

- Manual on iOS and Chrome mobile: pinch-to-zoom now works across the
app.
- Manual on desktop: Ctrl+/- zoom already worked via
`initial-scale=1.0`; unchanged.

## Risks

Low. Users who were relying on auto-zoom-suppression for text inputs
will notice nothing (modern Safari only auto-zooms below 16px). No API
surface change.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:43:45 -05:00
Jannes Stubbemann
f460f744ef fix: trust PAPERCLIP_PUBLIC_URL in board mutation guard (#3731)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Humans interact with the system through a web UI that authenticates
a session and then issues mutations against the board
> - A CSRF-style guard (`boardMutationGuard`) protects those mutations
by requiring the request origin match a trusted set built from the
`Host` / `X-Forwarded-Host` header
> - Behind certain reverse proxies, neither header matches the public
URL — TLS terminates at the edge and the inbound `Host` carries an
internal service name (cluster-local hostname, IP, or an Ingress backend
reference)
> - Mutations from legitimate browser sessions then fail with `403 Board
mutation requires trusted browser origin`
> - `PAPERCLIP_PUBLIC_URL` is already the canonical "what operators told
us the public URL is" value — it's used by better-auth and `config.ts`
> - This pull request adds it to the trusted-origin set when set, so
browsers reaching the legit public URL aren't blocked

## What Changed

- `server/src/middleware/board-mutation-guard.ts` — parse
`PAPERCLIP_PUBLIC_URL` and add its origin to the trusted set in
`trustedOriginsForRequest`. Additive only.

## Verification

- `PAPERCLIP_PUBLIC_URL=https://example.com pnpm start` then issue a
mutation from a browser pointed at `https://example.com`: 200, as
before. From an unrecognized origin: 403, as before.
- Without `PAPERCLIP_PUBLIC_URL` set: behavior is unchanged.

## Risks

Low. Additive only. The default dev origins and the
`Host`/`X-Forwarded-Host`-derived origins continue to be trusted; this
just adds the operator-configured public URL on top.

## Model Used

Claude Opus 4.6 (1M context), extended thinking mode.

## Checklist

- [x] Thinking path traces from project context to this change
- [x] Model used specified
- [x] Tests run locally and pass
- [x] CI green
- [x] Greptile review addressed
2026-04-15 09:42:55 -05:00
Dotta
32a9165ddf [codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base

## What Changed

- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.

## Verification

- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.

## Risks

- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
Chris Farhood
50cd76d8a3 feat(adapters): add capability flags to ServerAdapterModule (#3540)
## Thinking Path

> - Paperclip orchestrates AI agents via adapters (`claude_local`,
`codex_local`, etc.)
> - Each adapter type has different capabilities — instructions bundles,
skill materialization, local JWT — but these were gated by 5 hardcoded
type lists scattered across server routes and UI components
> - External adapter plugins (e.g. a future `opencode_k8s`) cannot add
themselves to those hardcoded lists without patching Paperclip source
> - The existing `supportsLocalAgentJwt` field on `ServerAdapterModule`
proves the right pattern already exists; it just wasn't applied to the
other capability gates
> - This pull request replaces the 4 remaining hardcoded lists with
declarative capability flags on `ServerAdapterModule`, exposed through
the adapter listing API
> - The benefit is that external adapter plugins can now declare their
own capabilities without any changes to Paperclip source code

## What Changed

- **`packages/adapter-utils/src/types.ts`** — added optional capability
fields to `ServerAdapterModule`: `supportsInstructionsBundle`,
`instructionsPathKey`, `requiresMaterializedRuntimeSkills`
- **`server/src/routes/agents.ts`** — replaced
`DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES` and
`ADAPTERS_REQUIRING_MATERIALIZED_RUNTIME_SKILLS` hardcoded sets with
capability-aware helper functions that fall back to the legacy sets for
adapters that don't set flags
- **`server/src/routes/adapters.ts`** — `GET /api/adapters` now includes
a `capabilities` object per adapter (all four flags + derived
`supportsSkills`)
- **`server/src/adapters/registry.ts`** — all built-in adapters
(`claude_local`, `codex_local`, `process`, `cursor`) now declare flags
explicitly
- **`ui/src/adapters/use-adapter-capabilities.ts`** — new hook that
fetches adapter capabilities from the API
- **`ui/src/pages/AgentDetail.tsx`** — replaced hardcoded `isLocal`
allowlist with `capabilities.supportsInstructionsBundle` from the API
- **`ui/src/components/AgentConfigForm.tsx`** /
**`OnboardingWizard.tsx`** — replaced `NONLOCAL_TYPES` denylist with
capability-based checks
- **`server/src/__tests__/adapter-registry.test.ts`** /
**`adapter-routes.test.ts`** — tests covering flag exposure,
undefined-when-unset, and per-adapter values
- **`docs/adapters/creating-an-adapter.md`** — new "Capability Flags"
section documenting all flags and an example for external plugin authors

## Verification

- Run `pnpm test --filter=@paperclip/server -- adapter-registry
adapter-routes` — all new tests pass
- Run `pnpm test --filter=@paperclip/adapter-utils` — existing tests
still pass
- Spin up dev server, open an agent with `claude_local` type —
instructions bundle tab still visible
- Create/open an agent with a non-local type — instructions bundle tab
still hidden
- Call `GET /api/adapters` and verify each adapter includes a
`capabilities` object with the correct flags

## Risks

- **Low risk overall** — all new flags are optional with
backwards-compatible fallbacks to the existing hardcoded sets; no
adapter behaviour changes unless a flag is explicitly set
- Adapters that do not declare flags continue to use the legacy lists,
so there is no regression risk for built-in adapters
- The UI capability hook adds one API call to AgentDetail mount; this is
a pre-existing endpoint, so no new latency path is introduced

## Model Used

- Provider: Anthropic
- Model: Claude Sonnet 4.6 (`claude-sonnet-4-6`)
- Context: 200k token context window
- Mode: Agentic tool use (code editing, bash, grep, file reads)

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Pawla Abdul (Bot) <pawla@groombook.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 07:10:52 -05:00
Knife.D
f6ce976544 fix: Anthropic subscription quota always shows 100% used (#3589)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The Costs > Providers tab displays live subscription quota from each
adapter (Claude, Codex)
> - The Claude adapter fetches utilization from the Anthropic OAuth
usage API and converts it to a 0-100 percent via `toPercent()`
> - The API changed to return utilization as 0-100 percentages (e.g.
`34.0` = 34%), but `toPercent()` assumed 0-1 fractions and multiplied by
100
> - After `Math.min(100, ...)` clamping, every quota window displayed as
100% used regardless of actual usage
> - Additionally, `extra_usage.used_credits` and `monthly_limit` are
returned in cents but were formatted as dollars, showing $6,793 instead
of $67.93
> - This PR applies the same `< 1` heuristic already proven in the Codex
adapter and fixes the cents-to-dollars conversion
> - The benefit is accurate quota display matching what users see on
claude.ai/settings/usage

## What Changed

- `toPercent()`: apply `< 1` heuristic to handle both legacy 0-1
fractions and current 0-100 percentage API responses (consistent with
Codex adapter's `normalizeCodexUsedPercent()`)
- `formatExtraUsageLabel()`: divide `used_credits` and `monthly_limit`
by 100 to convert cents to dollars before formatting
- Updated all `toPercent` and `fetchClaudeQuota` tests to use current
API format (0-100 range)
- Added backward-compatibility test for legacy 0-1 fraction values
- Added test for enabled extra usage with utilization and
cents-to-dollars conversion

## Verification

- `toPercent(34.0)` → `34` (was `100`)
- `toPercent(91.0)` → `91` (was `100`)
- `toPercent(0.5)` → `50` (legacy format still works)
- Extra usage `used_credits: 6793, monthly_limit: 14000` → `$67.93 /
$140.00` (was `$6,793.00 / $14,000.00`)
- Verified on a live instance with Claude Max subscription — Costs >
Providers tab now shows correct percentages matching
claude.ai/settings/usage

## Risks

Low risk. The `< 1` heuristic is already battle-tested in the Codex
adapter. The only edge case is a true utilization of exactly `1.0` which
maps to `1%` instead of `100%` — this is consistent with the Codex
adapter behavior and is an acceptable trade-off since 1% and 100% are
distinguishable in practice (100% would be returned as `100.0` by the
API).

## Model Used

Claude Opus 4.6 (1M context) via Claude Code CLI — tool use, code
analysis, and code generation

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Closes #2188

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:44:26 -05:00
Chris Farhood
b816809a1e fix(server): respect externally set PAPERCLIP_API_URL env var (#3472)
## Thinking Path

> - Paperclip server starts up and sets internal `PAPERCLIP_API_URL` for
downstream services and adapters
> - The server startup code was unconditionally overwriting
`PAPERCLIP_API_URL` with `http://localhost:3100` (or equivalent based on
`config.host`)
> - In Kubernetes deployments, `PAPERCLIP_API_URL` is set via a
ConfigMap to the externally accessible load balancer URL (e.g.
`https://paperclip.example.com`)
> - Because the env var was unconditionally set after loading the
ConfigMap value, the ConfigMap-provided URL was ignored and replaced
with the internal localhost address
> - This caused downstream services (adapter env building) to use the
wrong URL, breaking external access
> - This pull request makes the assignment conditional — only set if not
already provided by the environment
> - External deployments can now supply `PAPERCLIP_API_URL` and it will
be respected; local development continues to work without setting it

## What Changed

- `server/src/index.ts`: Wrapped `PAPERCLIP_API_URL` assignment in `if
(!process.env.PAPERCLIP_API_URL)` guard so externally provided values
are preserved
- `server/src/__tests__/server-startup-feedback-export.test.ts`: Added
tests verifying external `PAPERCLIP_API_URL` is respected and fallback
behavior is correct
- `docs/deploy/environment-variables.md`: Updated `PAPERCLIP_API_URL`
description to clarify it can be externally provided and the load
balancer/reverse proxy use case

## Verification

- Run the existing test suite: `pnpm test:run
server/src/__tests__/server-startup-feedback-export.test.ts` — all 3
tests pass
- Manual verification: Set `PAPERCLIP_API_URL` to a custom value before
starting the server and confirm it is not overwritten

## Risks

- Low risk — purely additive conditional check; existing behavior for
unset env var is unchanged

## Model Used

MiniMax M2.7 — reasoning-assisted for tracing the root cause through the
startup chain (`buildPaperclipEnv` → `startServer` → `config.host` →
`HOST` env var)

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Pawla Abdul (Bot) <pawla@groombook.dev>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 06:43:48 -05:00
Lempkey
d0a8d4e08a fix(routines): include cronExpression and timezone in list trigger response (#3209)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Routines are recurring tasks that trigger agents on a schedule or
via webhook
> - Routine triggers store their schedule as a `cronExpression` +
`timezone` in the database
> - The `GET /companies/:companyId/routines` list endpoint is the
primary way API consumers (and the UI) discover all routines and their
triggers
> - But the list endpoint was silently dropping `cronExpression` and
`timezone` from each trigger object — the DB query fetched them, but the
explicit object-construction mapping only forwarded seven other fields
> - This PR fixes the mapping to include `cronExpression` and
`timezone`, and extends the `RoutineListItem.triggers` type to match
> - The benefit is that API consumers can now see the actual schedule
from the list endpoint, and future UI components reading from the list
cache will get accurate schedule data

## What Changed

- **`server/src/services/routines.ts`** — Added `cronExpression` and
`timezone` to the explicit trigger object mapping inside
`routinesService.list()`. The DB query (`listTriggersForRoutineIds`)
already fetched all columns via `SELECT *`; the values were being
discarded during object construction.
- **`packages/shared/src/types/routine.ts`** — Extended
`RoutineListItem.triggers` `Pick<RoutineTrigger, ...>` to include
`cronExpression` and `timezone` so the TypeScript type contract matches
the actual runtime shape.
- **`server/src/__tests__/routines-e2e.test.ts`** — Added assertions to
the existing schedule-trigger E2E test that verify both `cronExpression`
and `timezone` are present in the `GET /companies/:companyId/routines`
list response.

## Verification

```bash
# Run the route + service unit tests
npx vitest run server/src/__tests__/routines-routes.test.ts server/src/__tests__/routines-service.test.ts
# → 21 tests pass

# Confirm cronExpression appears in list response
curl /api/companies/{id}/routines | jq '.[].triggers[].cronExpression'
# → now returns the actual cron string instead of undefined
```

Manual reproduction per the issue:
1. Create a routine with a schedule trigger (`cronExpression: "47 14 * *
*"`, `timezone: "America/Mexico_City"`)
2. `GET /api/companies/{id}/routines` — trigger object now includes
`cronExpression` and `timezone`

## Risks

Low risk. The change only adds two fields to an existing response shape
— no fields removed, no behavior changed. The `cronExpression` is `null`
for non-schedule trigger kinds (webhook, etc.), consistent with
`RoutineTrigger.cronExpression: string | null`. No migration required.

## Model Used

- **Provider:** Anthropic
- **Model:** Claude Sonnet 4.6 (`claude-sonnet-4-6`)
- **Context window:** 200k tokens
- **Mode:** Extended thinking + tool use (agentic)
- Secondary adversarial review: OpenAI Codex (via codex-companion
plugin)

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (API-only fix; no UI rendering change)
- [ ] I have updated relevant documentation to reflect my changes (no
doc changes needed)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-15 06:42:24 -05:00
Clément DREISKI
213bcd8c7a fix: include routine-execution issues in agent inbox-lite (#3329)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Agents query their own inbox via `/agents/me/inbox-lite` to discover
assigned work
> - `issuesSvc.list()` excludes `routine_execution` issues by default,
which is appropriate for the board UI
> - But agents calling `inbox-lite` need to see **all** their assigned
work, including routine-created issues
> - Without `includeRoutineExecutions: true`, agents miss their own
in-progress issues after the first delegation step
> - This causes routine-driven pipelines to stall — agents report "Inbox
empty" and exit
> - This pull request adds `includeRoutineExecutions: true` to the
`inbox-lite` query
> - The benefit is routine-driven pipelines no longer stall after
delegation

## What Changed

- Added `includeRoutineExecutions: true` to the `issuesSvc.list()` call
in the `/agents/me/inbox-lite` route (`server/src/routes/agents.ts`)

## Verification

1. Create a routine that assigns an issue to an agent
2. Trigger the routine — first run works via `issue_assigned` event
injection
3. Agent delegates (creates a subtask) and exits
4. On next heartbeat, agent queries `inbox-lite`
5. **Before fix**: issue is invisible, agent reports "Inbox empty"
6. **After fix**: issue appears in inbox, agent continues working

Tested on production instance — fix resolves the stall immediately.

## Risks

Low risk — additive change, only affects agent-facing inbox endpoint.
Board UI keeps its default behavior (routine executions hidden for clean
view).

## Model Used

Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI — high thinking
effort, tool use.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Closes #3282
2026-04-15 06:41:40 -05:00
Dotta
7f893ac4ec [codex] Harden execution reliability and heartbeat tooling (#3679)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Reliable execution depends on heartbeat routing, issue lifecycle
semantics, telemetry, and a fast enough local verification loop to keep
regressions visible
> - The remaining commits on this branch were mostly server/runtime
correctness fixes plus test and documentation follow-ups in that area
> - Those changes are logically separate from the UI-focused
issue-detail and workspace/navigation branches even when they touch
overlapping issue APIs
> - This pull request groups the execution reliability, heartbeat,
telemetry, and tooling changes into one standalone branch
> - The benefit is a focused review of the control-plane correctness
work, including the follow-up fix that restored the implicit
comment-reopen helpers after branch splitting

## What Changed

- Hardened issue/heartbeat execution behavior, including self-review
stage skipping, deferred mention wakes during active execution, stranded
execution recovery, active-run scoping, assignee resolution, and
blocked-to-todo wake resumption
- Reduced noisy polling/logging overhead by trimming issue run payloads,
compacting persisted run logs, silencing high-volume request logs, and
capping heartbeat-run queries in dashboard/inbox surfaces
- Expanded telemetry and status semantics with adapter/model fields on
task completion plus clearer status guidance in docs/onboarding material
- Updated test infrastructure and verification defaults with faster
route-test module isolation, cheaper default `pnpm test`, e2e isolation
from local state, and repo verification follow-ups
- Included docs/release housekeeping from the branch and added a small
follow-up commit restoring the implicit comment-reopen helpers that were
dropped during branch reconstruction

## Verification

- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts`
- `pnpm vitest run server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/health.test.ts`
- `server/src/__tests__/activity-service.test.ts`,
`server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and
`server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted
on this host but the embedded Postgres harness reported
init-script/data-dir problems and skipped or failed to start, so they
are noted as environment-limited

## Risks

- Medium: this branch changes core issue/heartbeat routing and
reopen/wakeup behavior, so regressions would affect agent execution flow
rather than isolated UI polish
- Because it also updates verification infrastructure, reviewers should
pay attention to whether the new tests are asserting the right failure
modes and not just reshaping harness behavior

## Model Used

- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 13:34:52 -05:00
Dotta
e89076148a [codex] Improve workspace runtime and navigation ergonomics (#3680)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - That operator experience depends not just on issue chat, but also on
how workspaces, inbox groups, and navigation state behave over
long-running sessions
> - The current branch included a separate cluster of workspace-runtime
controls, inbox grouping, sidebar ordering, and worktree lifecycle fixes
> - Those changes cross server, shared contracts, database state, and UI
navigation, but they still form one coherent operator workflow area
> - This pull request isolates the workspace/runtime and navigation
ergonomics work into one standalone branch
> - The benefit is better workspace recovery and navigation persistence
without forcing reviewers through the unrelated issue-detail/chat work

## What Changed

- Improved execution workspace and project workspace controls, request
wiring, layout, and JSON editor ergonomics
- Hardened linked worktree reuse/startup behavior and documented the
`worktree repair` flow for recovering linked worktrees safely
- Added inbox workspace grouping, mobile collapse, archive undo,
keyboard navigation, shared group-header styling, and persisted
collapsed-group behavior
- Added persistent sidebar order preferences with the supporting DB
migration, shared/server contracts, routes, services, hooks, and UI
integration
- Scoped issue-list preferences by context and added targeted UI/server
tests for workspace controls, inbox behavior, sidebar preferences, and
worktree validation

## Verification

- `pnpm vitest run
server/src/__tests__/sidebar-preferences-routes.test.ts
ui/src/pages/Inbox.test.tsx
ui/src/components/ProjectWorkspaceSummaryCard.test.tsx
ui/src/components/WorkspaceRuntimeControls.test.tsx
ui/src/api/workspace-runtime-control.test.ts`
- `server/src/__tests__/workspace-runtime.test.ts` was attempted, but
the embedded Postgres suite self-skipped/hung on this host after
reporting an init-script issue, so it is not counted as a local pass
here

## Risks

- Medium: this branch includes migration-backed preference storage plus
worktree/runtime behavior, so merge review should pay attention to state
persistence and worktree recovery semantics
- The sidebar preference migration is standalone, but it should still be
watched for conflicts if another migration lands first

## Model Used

- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 12:57:11 -05:00
Dotta
6e6f538630 [codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors

## What Changed

- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors

## Verification

- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`

## Risks

- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge

## Model Used

- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 12:50:48 -05:00
Dotta
5d1ed71779 chore: add v2026.410.0 release notes for security release
## Summary

- Adds release notes for v2026.410.0 security release covering
GHSA-68qg-g8mg-6pr7
- Required before triggering the stable release workflow to publish
2026.410.0 to npm

## Context

The security advisory GHSA-68qg-g8mg-6pr7 was published on 2026-04-10
listing 2026.410.0 as the patched version, but only canary builds exist
on npm. The authz fix (PR #3315) is already merged to master. This PR
adds release notes so the stable release workflow can be triggered.

## Test plan

- Verify release notes content is accurate
- Merge, then trigger release.yml workflow_dispatch with
source_ref=master, stable_date=2026-04-10, dry_run=false
- Confirm npm view paperclipai version returns 2026.410.0

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-13 12:43:17 -05:00
Dotta
76fe736e8e chore: add v2026.410.0 release notes for security release
## Summary

- Adds comprehensive release notes for `v2026.410.0`, the security
release that patches GHSA-68qg-g8mg-6pr7 (unauthenticated RCE via import
authorization bypass)
- Required before triggering the stable release workflow to publish
`2026.410.0` to npm and create the GitHub Release

## Context

The security fix (PR #3315) is already merged to master. The GHSA
advisory references `2026.410.0` as the patched version, but only canary
builds exist on npm. This PR unblocks the stable release.

## Test plan

- [x] Release notes file is valid markdown
- [ ] Merge and trigger `release.yml` workflow with `source_ref=master`,
`stable_date=2026-04-10`
- [ ] Verify `npm view paperclipai version` returns `2026.410.0`
- [ ] Verify GitHub Release `v2026.410.0` exists

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

Co-Authored-By: Paperclip <noreply@paperclip.ing>

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-13 12:43:07 -05:00
Dotta
d6b06788f6 Merge pull request #3542 from cryppadotta/PAP-1346-faster-issue-to-issue-links
Speed up issue-to-issue navigation
2026-04-12 21:39:27 -05:00
Dotta
6844226572 Address Greptile navigation review 2026-04-12 21:30:50 -05:00
Dotta
0cb42f49ea Fix rebased issue detail prefetch typing 2026-04-12 21:18:57 -05:00
Dotta
e59047187b Reset scroll on issue detail navigation 2026-04-12 21:14:12 -05:00
Dotta
1729e41179 Speed up issue-to-issue navigation 2026-04-12 21:14:12 -05:00
Dotta
11de5ae9c9 Merge pull request #3538 from paperclipai/PAP-1355-right-now-when-agents-boot-they-re-instructed-to-call-the-api-to-checkout-the-issue-so-that-they-have-exclusive
Improve scoped wake checkout and linked worktree reuse
2026-04-12 21:08:20 -05:00
Dotta
8e82ac7e38 Handle harness checkout conflicts gracefully 2026-04-12 20:57:31 -05:00
Dotta
be82a912b2 Fix signoff e2e for auto-checked out issues 2026-04-12 20:43:50 -05:00
Dotta
ab5eeca94e Fix stale issue live-run state 2026-04-12 20:41:31 -05:00
Dotta
2172476e84 Fix linked worktree reuse for execution workspaces 2026-04-12 20:34:06 -05:00
Dotta
c1bb938519 Auto-checkout scoped issue wakes in the harness 2026-04-11 10:53:28 -05:00
Dotta
b649bd454f Merge pull request #3383 from paperclipai/pap-1347-codex-fast-mode
feat(codex-local): add fast mode support
2026-04-11 08:45:50 -05:00
Dotta
a692e37f3e Merge pull request #3386 from paperclipai/pap-1347-dev-runner-worktree-env
fix: isolate dev runner worktree env
2026-04-11 08:45:16 -05:00
Dotta
96637a1e09 Merge pull request #3385 from paperclipai/pap-1347-inbox-issue-search
feat(inbox): improve issue search matches
2026-04-11 08:44:59 -05:00
Dotta
a5aed931ab fix(dev-runner): tighten worktree env bootstrap 2026-04-11 08:35:53 -05:00
Dotta
a63e847525 fix(inbox): avoid refetching on filter-only changes 2026-04-11 08:34:17 -05:00
Dotta
a7dc88941b fix(codex-local): avoid fast mode in env probe 2026-04-11 08:33:18 -05:00
Dotta
b6115424b1 fix: isolate dev runner worktree env 2026-04-11 08:27:25 -05:00
Dotta
1f78e55072 Broaden comment matches in issue search 2026-04-11 08:26:09 -05:00
Dotta
fcab770518 Add inbox issue search fallback 2026-04-11 08:26:09 -05:00
Dotta
2d8f97feb0 feat(codex-local): add fast mode support 2026-04-11 08:21:55 -05:00
Dotta
03a2cf5c8a Merge pull request #3303 from cryppadotta/PAP-438-review-openclaw-s-docs-on-networking-discovery-and-binding-what-could-we-learn-from-this
Introduce bind presets for deployment setup
2026-04-11 07:24:57 -05:00
Dotta
a77206812e Harden tailnet bind setup 2026-04-11 07:13:41 -05:00
dotta
6208899d0a Fix dev runner workspace import regression 2026-04-11 07:09:07 -05:00
dotta
2a84e53c1b Introduce bind presets for deployment setup
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 07:09:07 -05:00
Dotta
e1bf9d66a7 Merge pull request #3355 from cryppadotta/pap-1331-issue-thread-ux
feat: polish issue thread markdown and references
2026-04-11 06:55:26 -05:00
Dotta
b48be80d5d fix: address PR 3355 review regressions 2026-04-11 06:40:37 -05:00
Dotta
45ebecab5a Merge pull request #3356 from cryppadotta/pap-1331-inbox-ux
feat: polish inbox and issue list workflows
2026-04-11 06:35:59 -05:00
Dotta
dae888cc5d Merge pull request #3354 from cryppadotta/pap-1331-runtime-workflows
fix: harden heartbeat and adapter runtime workflows
2026-04-11 06:31:28 -05:00
Dotta
aaf42f3a7e Merge pull request #3353 from cryppadotta/pap-1331-dev-tools-docs
chore: improve worktree tooling and security docs
2026-04-11 06:28:49 -05:00
Dotta
62d05a7ae2 Merge pull request #3232 from officialasishkumar/fix/clear-empty-agent-env-bindings
fix(ui): persist cleared agent env bindings on save
2026-04-11 06:23:14 -05:00
Dotta
1cd0281b4d test(ui): fix heartbeat run fixture drift 2026-04-10 22:42:52 -05:00
Dotta
65480ffab1 fix: restore inbox optimistic run fixture 2026-04-10 22:40:49 -05:00
Dotta
dc94e3d1df fix: keep thread polish independent of quicklook routing 2026-04-10 22:36:45 -05:00
Dotta
0162bb332c fix: keep runtime UI changes self-contained 2026-04-10 22:36:45 -05:00
Dotta
7ec8716159 fix: keep inbox quicklook and tests standalone 2026-04-10 22:36:45 -05:00
Dotta
8cb70d897d fix: use CLI tsx entrypoint for workspace preflight 2026-04-10 22:32:55 -05:00
Dotta
8bdf4081ee chore: improve worktree tooling and security docs 2026-04-10 22:26:30 -05:00
Dotta
958c11699e feat: polish issue thread markdown and references 2026-04-10 22:26:21 -05:00
Dotta
c566a9236c fix: harden heartbeat and adapter runtime workflows 2026-04-10 22:26:21 -05:00
Dotta
dab95740be feat: polish inbox and issue list workflows 2026-04-10 22:26:21 -05:00
Devin Foley
548721248e fix(ui): keep latest issue document revision current (#3342)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Board users and agents collaborate on issue-scoped documents such as
plans and revisions need to be trustworthy because they are the audit
trail for those artifacts.
> - The issue document UI now supports revision history and restore, so
the UI has to distinguish the current revision from historical revisions
correctly even while multiple queries are refreshing.
> - In `PAPA-72`, the newest content could appear under an older
revision label because the current document snapshot and the
revision-history query could temporarily disagree after an edit.
> - That made the UI treat the newest revision like a historical restore
target, which is the opposite of the intended behavior.
> - This pull request derives one authoritative revision view from both
sources, sorts revisions newest-first, and keeps the freshest revision
marked current.
> - The benefit is that revision history stays stable and trustworthy
immediately after edits instead of briefly presenting the newest content
as an older revision.

## What Changed

- Added a `document-revisions` helper that merges the current document
snapshot with fetched revision history into one normalized revision
state.
- Updated `IssueDocumentsSection` to render from that normalized state
instead of trusting either query in isolation.
- Added focused tests covering the current-revision selection and
ordering behavior.

## Verification

- `pnpm -r typecheck`
- `pnpm build`
- Targeted revision tests passed locally.
- Manual reviewer check:
  - Open an issue document with revision history.
  - Edit and save the document.
  - Immediately open the revision selector.
- Confirm the newest revision remains marked current and older revisions
remain the restore targets.

## Risks

- Low risk. The change is isolated to issue document revision
presentation in the UI.
- Main risk is merging the current snapshot with fetched history
incorrectly for edge cases, which is why the helper has focused unit
coverage.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-10 17:14:06 -07:00
Devin Foley
f4a05dc35c fix(cli): prepare plugin sdk before cli dev boot (#3343)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The company import/export e2e exercises the local CLI startup path
that boots the dev server inside a workspace
> - That startup path loads server and plugin code which depends on
built workspace package artifacts such as `@paperclipai/shared` and
`@paperclipai/plugin-sdk`
> - In a clean worktree those `dist/*` artifacts may not exist yet even
though `paperclipai run` can still attempt to import the local server
entry
> - That mismatch caused the import/export e2e to fail before the actual
company package flow ran
> - This pull request adds a CLI preflight step that prepares the needed
workspace build dependencies before the local server import and fails
closed if that preflight is interrupted or stalls
> - The benefit is that clean worktrees can boot `paperclipai run`
reliably without silently continuing after incomplete dependency
preparation

## What Changed

- Updated `cli/src/commands/run.ts` to execute
`scripts/ensure-plugin-build-deps.mjs` before importing
`server/src/index.ts` for local dev startup.
- Ensured `paperclipai run` can materialize missing workspace artifacts
such as `packages/shared/dist` and `packages/plugins/sdk/dist`
automatically in clean worktrees.
- Made the preflight fail closed when the child process exits via signal
and bounded it with a 120-second timeout so the CLI does not hang
indefinitely.
- Kept the fix isolated to the CLI startup path; no API contract,
schema, or UI behavior changed.
- Reused the existing
`cli/src/__tests__/company-import-export-e2e.test.ts` coverage that
already exercises the failing boot path, so no additional test file was
needed.

## Verification

- `pnpm test:run cli/src/__tests__/company-import-export-e2e.test.ts`
- `pnpm --filter paperclipai typecheck`
- On the isolated branch, confirmed `packages/shared/dist/index.js` and
`packages/plugins/sdk/dist/index.js` were absent before the run, then
reran the targeted e2e and observed a passing result.

## Risks

- Low risk: the change only affects the local CLI dev startup path
before the server import.
- Residual risk: other entrypoints still rely on their own
preflight/build behavior, so this does not normalize every workspace
startup path.
- The 120-second timeout is intentionally generous, but unusually slow
machines could still hit it and surface a startup error instead of
waiting forever.

## Model Used

- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment,
with shell/tool execution enabled. The exact runtime revision and
context window are not exposed by this environment.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-10 17:01:06 -07:00
Dotta
b00d52c5b6 Merge pull request #3015 from aronprins/feature/backups-configuration
feat(backups): gzip compression and tiered retention with UI controls
2026-04-10 11:56:12 -05:00
Aron Prins
7c42345177 chore: re-trigger CI to refresh PR base SHA 2026-04-10 12:16:25 +02:00
Asish Kumar
44d94d0add fix(ui): persist cleared agent env bindings on save
Agent configuration edits already had an API path for replacing the full adapterConfig, but the edit form was still sending merge-style patches. That meant clearing the last environment variable serialized as undefined, the key disappeared from JSON, and the server merged the old env bindings back into the saved config.

Build adapter config save payloads as full replacement patches, strip undefined keys before send, and reuse the existing replaceAdapterConfig contract so explicit clears persist correctly. Add regression coverage for the cleared-env case and for adapter-type changes that still need to preserve adapter-agnostic fields.

Fixes #3179
2026-04-09 17:50:14 +00:00
Aron Prins
b1e457365b fix: clean up orphaned .sql on compression failure and fix stale startup log
- backup-lib: delete uncompressed .sql file in catch block when gzip
  compression fails, preventing silent disk usage accumulation
- server: replace stale retentionDays scalar with retentionSource in
  startup log since retention is now read from DB on each backup tick
2026-04-08 14:40:05 +02:00
Aron Prins
fcbae62baf feat(backups): tiered daily/weekly/monthly retention with UI controls
Replace single retentionDays with a three-tier BackupRetentionPolicy:
- Daily: keep all backups (presets: 3, 7, 14 days; default 7)
- Weekly: keep one per calendar week (presets: 1, 2, 4 weeks; default 4)
- Monthly: keep one per calendar month (presets: 1, 3, 6 months; default 1)

Pruning sorts backups newest-first and applies each tier's cutoff,
keeping only the newest entry per ISO week/month bucket. The Instance
Settings General page now shows three preset selectors (no icon, matches
existing page design). Remove Database icon import.
2026-04-08 14:40:05 +02:00
Aron Prins
cc44d309c0 feat(backups): gzip compress backups and add retention config to Instance Settings
Compress database backups with gzip (.sql.gz), reducing file size ~83%.
Add backup retention configuration to Instance Settings UI with preset
options (7 days, 2 weeks, 1 month). The backup scheduler now reads
retention from the database on each tick so changes take effect without
restart. Default retention changed from 30 to 7 days.
2026-04-08 14:40:05 +02:00
780 changed files with 172245 additions and 7111 deletions

View File

@@ -154,6 +154,14 @@ Each AGENTS.md body should include not just what the agent does, but how they fi
This turns a collection of agents into an organization that actually works together. Without workflow context, agents operate in isolation — they do their job but don't know what happens before or after them.
Add a concise execution contract to every generated working agent:
- Start actionable work in the same heartbeat and do not stop at a plan unless planning was requested.
- Leave durable progress in comments, documents, or work products with the next action.
- Use child issues for long or parallel delegated work instead of polling agents, sessions, or processes.
- Mark blocked work with the unblock owner and action.
- Respect budget, pause/cancel, approval gates, and company boundaries.
### Step 5: Confirm Output Location
Ask the user where to write the package. Common options:

View File

@@ -105,6 +105,13 @@ Your responsibilities:
- Implement features and fix bugs
- Write tests and documentation
- Participate in code reviews
Execution contract:
- Start actionable implementation work in the same heartbeat; do not stop at a plan unless planning was requested.
- Leave durable progress with a clear next action.
- Use child issues for long or parallel delegated work instead of polling agents, sessions, or processes.
- Mark blocked work with the unblock owner and action.
```
## teams/engineering/TEAM.md

View File

@@ -548,7 +548,7 @@ Import from `@paperclipai/adapter-utils/server-utils`:
### Prompt Templates
- Support `promptTemplate` for every run
- Use `renderTemplate()` with the standard variable set
- Default prompt: `"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work."`
- Default prompt should use `DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE` from `@paperclipai/adapter-utils/server-utils` so local adapters share Paperclip's execution contract: act in the same heartbeat, avoid planning-only exits unless requested, leave durable progress and a next action, use child issues instead of polling, mark blockers with owner/action, and respect governance boundaries.
### Error Handling
- Differentiate timeout vs process error vs parse failure

View File

@@ -0,0 +1,230 @@
---
name: deal-with-security-advisory
description: >
Handle a GitHub Security Advisory response for Paperclip, including
confidential fix development in a temporary private fork, human coordination
on advisory-thread comments, CVE request, synchronized advisory publication,
and immediate security release steps.
---
# Security Vulnerability Response Instructions
## ⚠️ CRITICAL: This is a security vulnerability. Everything about this process is confidential until the advisory is published. Do not mention the vulnerability details in any public commit message, PR title, branch name, or comment. Do not push anything to a public branch. Do not discuss specifics in any public channel. Assume anything on the public repo is visible to attackers who will exploit the window between disclosure and user upgrades.
***
## Context
A security vulnerability has been reported via GitHub Security Advisory:
* **Advisory:** {{ghsaId}} (e.g. GHSA-x8hx-rhr2-9rf7)
* **Reporter:** {{reporterHandle}}
* **Severity:** {{severity}}
* **Notes:** {{notes}}
***
## Step 0: Fetch the Advisory Details
Pull the full advisory so you understand the vulnerability before doing anything else:
```
gh api repos/paperclipai/paperclip/security-advisories/{{ghsaId}}
```
Read the `description`, `severity`, `cvss`, and `vulnerabilities` fields. Understand the attack vector before writing code.
## Step 1: Acknowledge the Report
⚠️ **This step requires a human.** The advisory thread does not have a comment API. Ask the human operator to post a comment on the private advisory thread acknowledging the report. Provide them this template:
> Thanks for the report, @{{reporterHandle}}. We've confirmed the issue and are working on a fix. We're targeting a patch release within {{timeframe}}. We'll keep you updated here.
Give your human this template, but still continue
Below we use `gh` tools - you do have access and credentials outside of your sandbox, so use them.
## Step 2: Create the Temporary Private Fork
This is where all fix development happens. Never push to the public repo.
```
gh api --method POST \
repos/paperclipai/paperclip/security-advisories/{{ghsaId}}/forks
```
This returns a repository object for the private fork. Save the `full_name` and `clone_url`.
Clone it and set up your workspace:
```
# Clone the private fork somewhere outside ~/paperclip
git clone <clone_url_from_response> ~/security-patch-{{ghsaId}}
cd ~/security-patch-{{ghsaId}}
git checkout -b security-fix
```
**Do not edit `~/paperclip`** — the dev server is running off the `~/paperclip` master branch and we don't want to touch it. All work happens in the private fork clone.
**TIPS:**
* Do not commit `pnpm-lock.yaml` — the repo has actions to manage this
* Do not use descriptive branch names that leak the vulnerability (e.g., no `fix-dns-rebinding-rce`). Use something generic like `security-fix`
* All work stays in the private fork until publication
* CI/GitHub Actions will NOT run on the temporary private fork — this is a GitHub limitation by design. You must run tests locally
## Step 3: Develop and Validate the Fix
Write the patch. Same content standards as any PR:
* It must functionally work — **run tests locally** since CI won't run on the private fork
* Consider the whole codebase, not just the narrow vulnerability path. A patch that fixes one vector but opens another is worse than no patch
* Ensure backwards compatibility for the database, or be explicit about what breaks
* Make sure any UI components still look correct if the fix touches them
* The fix should be minimal and focused — don't bundle unrelated changes into a security patch. Reviewers (and the reporter) should be able to read the diff and understand exactly what changed and why
**Specific to security fixes:**
* Verify the fix actually closes the attack vector described in the advisory. Reproduce the vulnerability first (using the reporter's description), then confirm the patch prevents it
* Consider adjacent attack vectors — if DNS rebinding is the issue, are there other endpoints or modes with the same class of problem?
* Do not introduce new dependencies unless absolutely necessary — new deps in a security patch raise eyebrows
Push your fix to the private fork:
```
git add -A
git commit -m "Fix security vulnerability"
git push origin security-fix
```
## Step 4: Coordinate with the Reporter
⚠️ **This step requires a human.** Ask the human operator to post on the advisory thread letting the reporter know the fix is ready and giving them a chance to review. Provide them this template:
> @{{reporterHandle}} — fix is ready in the private fork if you'd like to review before we publish. Planning to release within {{timeframe}}.
Proceed
## Step 5: Request a CVE
This makes vulnerability scanners (npm audit, Snyk, Dependabot) warn users to upgrade. Without it, nobody gets automated notification.
```
gh api --method POST \
repos/paperclipai/paperclip/security-advisories/{{ghsaId}}/cve
```
GitHub is a CVE Numbering Authority and will assign one automatically. The CVE may take a few hours to propagate after the advisory is published.
## Step 6: Publish Everything Simultaneously
This all happens at once — do not stagger these steps. The goal is **zero window** between the vulnerability becoming public knowledge and the fix being available.
### 6a. Verify reporter credit before publishing
```
gh api repos/paperclipai/paperclip/security-advisories/{{ghsaId}} --jq '.credits'
```
If the reporter is not credited, add them:
```
gh api --method PATCH \
repos/paperclipai/paperclip/security-advisories/{{ghsaId}} \
--input - << 'EOF'
{
"credits": [
{
"login": "{{reporterHandle}}",
"type": "reporter"
}
]
}
EOF
```
### 6b. Update the advisory with the patched version and publish
```
gh api --method PATCH \
repos/paperclipai/paperclip/security-advisories/{{ghsaId}} \
--input - << 'EOF'
{
"state": "published",
"vulnerabilities": [
{
"package": {
"ecosystem": "npm",
"name": "paperclip"
},
"vulnerable_version_range": "< {{patchedVersion}}",
"patched_versions": "{{patchedVersion}}"
}
]
}
EOF
```
Publishing the advisory simultaneously:
* Makes the GHSA public
* Merges the temporary private fork into your repo
* Triggers the CVE assignment (if requested in step 5)
### 6c. Cut a release immediately after merge
```
cd ~/paperclip
git pull origin master
gh release create v{{patchedVersion}} \
--repo paperclipai/paperclip \
--title "v{{patchedVersion}} — Security Release" \
--notes "## Security Release
This release fixes a critical security vulnerability.
### What was fixed
{{briefDescription}} (e.g., Remote code execution via DNS rebinding in \`local_trusted\` mode)
### Advisory
https://github.com/paperclipai/paperclip/security/advisories/{{ghsaId}}
### Credit
Thanks to @{{reporterHandle}} for responsibly disclosing this vulnerability.
### Action required
All users running versions prior to {{patchedVersion}} should upgrade immediately."
```
## Step 7: Post-Publication Verification
```
# Verify the advisory is published and CVE is assigned
gh api repos/paperclipai/paperclip/security-advisories/{{ghsaId}} \
--jq '{state: .state, cve_id: .cve_id, published_at: .published_at}'
# Verify the release exists
gh release view v{{patchedVersion}} --repo paperclipai/paperclip
```
If the CVE hasn't been assigned yet, that's normal — it can take a few hours.
⚠️ **Human step:** Ask the human operator to post a final comment on the advisory thread confirming publication and thanking the reporter.
Tell the human operator what you did by posting a comment to this task, including:
* The published advisory URL: `https://github.com/paperclipai/paperclip/security/advisories/{{ghsaId}}`
* The release URL
* Whether the CVE has been assigned yet
* All URLs to any pull requests or branches

View File

@@ -2,3 +2,6 @@ DATABASE_URL=postgres://paperclip:paperclip@localhost:5432/paperclip
PORT=3100
SERVE_UI=false
BETTER_AUTH_SECRET=paperclip-dev-secret
# Discord webhook for daily merge digest (scripts/discord-daily-digest.sh)
# DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...

View File

@@ -38,6 +38,8 @@
-
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`.
## Model Used
<!--
@@ -57,6 +59,7 @@
- [ ] I have included a thinking path that traces from project context to this change
- [ ] I have specified the model used (with version and capability details)
- [ ] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after screenshots

4
.gitignore vendored
View File

@@ -1,4 +1,7 @@
node_modules
node_modules/
**/node_modules
**/node_modules/
dist/
.env
*.tsbuildinfo
@@ -32,6 +35,7 @@ server/src/**/*.d.ts
server/src/**/*.d.ts.map
tmp/
feedback-export-*
diagnostics/
# Editor / tool temp files
*.tmp

View File

@@ -1 +1,3 @@
Dotta <bippadotta@protonmail.com> Forgotten <forgottenrunes@protonmail.com>
Dotta <bippadotta@protonmail.com> <34892728+cryppadotta@users.noreply.github.com>
Dotta <bippadotta@protonmail.com> <forgottenrunes@protonmail.com>
Dotta <bippadotta@protonmail.com> <dotta@example.com>

View File

@@ -108,6 +108,21 @@ Notes:
## 7. Verification Before Hand-off
Default local/agent test path:
```sh
pnpm test
```
This is the cheap default and only runs the Vitest suite. Browser suites stay opt-in:
```sh
pnpm test:e2e
pnpm test:release-smoke
```
Run the browser suites only when your change touches them or when you are explicitly verifying CI/release flows.
Run this full check before claiming done:
```sh

View File

@@ -51,6 +51,21 @@ All tests must pass before a PR can be merged. Run them locally first and verify
We use [Greptile](https://greptile.com) for automated code review. Your PR must achieve a **5/5 Greptile score** with **all Greptile comments addressed** before it can be merged. If Greptile leaves comments, fix or respond to each one and request a re-review.
## Feature Contributions
We actively manage the core Paperclip feature roadmap.
Uncoordinated feature PRs against the core product may be closed, even when the implementation is thoughtful and high quality. That is about roadmap ownership, product coherence, and long-term maintenance commitment, not a judgment about the effort.
If you want to contribute a feature:
- Check [ROADMAP.md](ROADMAP.md) first
- Start the discussion in Discord -> `#dev` before writing code
- If the idea fits as an extension, prefer building it with the [plugin system](doc/plugins/PLUGIN_SPEC.md)
- If you want to show a possible direction, reference implementations are welcome as feedback, but they generally will not be merged directly into core
Bugs, docs improvements, and small targeted improvements are still the easiest path to getting merged, and we really do appreciate them.
## General Rules (both paths)
- Write clear commit messages

View File

@@ -2,15 +2,7 @@ FROM node:lts-trixie-slim AS base
ARG USER_UID=1000
ARG USER_GID=1000
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates gosu curl git wget ripgrep python3 \
&& mkdir -p -m 755 /etc/apt/keyrings \
&& wget -nv -O/etc/apt/keyrings/githubcli-archive-keyring.gpg https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& echo "20e0125d6f6e077a9ad46f03371bc26d90b04939fb95170f5a1905099cc6bcc0 /etc/apt/keyrings/githubcli-archive-keyring.gpg" | sha256sum -c - \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& mkdir -p -m 755 /etc/apt/sources.list.d \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& apt-get install -y --no-install-recommends ca-certificates gosu curl gh git wget ripgrep python3 \
&& rm -rf /var/lib/apt/lists/* \
&& corepack enable
@@ -56,6 +48,9 @@ ARG USER_GID=1000
WORKDIR /app
COPY --chown=node:node --from=build /app /app
RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai \
&& apt-get update \
&& apt-get install -y --no-install-recommends openssh-client jq \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /paperclip \
&& chown node:node /paperclip

View File

@@ -177,6 +177,14 @@ Open source. Self-hosted. No Paperclip account required.
npx paperclipai onboard --yes
```
That quickstart path now defaults to trusted local loopback mode for the fastest first run. To start in authenticated/private mode instead, choose a bind preset explicitly:
```bash
npx paperclipai onboard --yes --bind lan
# or:
npx paperclipai onboard --yes --bind tailnet
```
If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to edit settings.
Or manually:
@@ -225,11 +233,15 @@ pnpm dev:once # Full dev without file watching
pnpm dev:server # Server only
pnpm build # Build all
pnpm typecheck # Type checking
pnpm test:run # Run tests
pnpm test # Cheap default test run (Vitest only)
pnpm test:watch # Vitest watch mode
pnpm test:e2e # Playwright browser suite
pnpm db:generate # Generate DB migration
pnpm db:migrate # Apply migrations
```
`pnpm test` does not run Playwright. Browser suites stay separate and are typically run only when working on those flows or in CI.
See [doc/DEVELOPING.md](doc/DEVELOPING.md) for the full development guide.
<br/>
@@ -243,14 +255,23 @@ See [doc/DEVELOPING.md](doc/DEVELOPING.md) for the full development guide.
- ✅ Skills Manager
- ✅ Scheduled Routines
- ✅ Better Budgeting
- Artifacts & Deployments
- ⚪ CEO Chat
- ⚪ MAXIMIZER MODE
- ⚪ Multiple Human Users
- Agent Reviews and Approvals
- ✅ Multiple Human Users
- ⚪ Cloud / Sandbox agents (e.g. Cursor / e2b agents)
- ⚪ Artifacts & Work Products
- ⚪ Memory / Knowledge
- ⚪ Enforced Outcomes
- ⚪ MAXIMIZER MODE
- ⚪ Deep Planning
- ⚪ Work Queues
- ⚪ Self-Organization
- ⚪ Automatic Organizational Learning
- ⚪ CEO Chat
- ⚪ Cloud deployments
- ⚪ Desktop App
This is the short roadmap preview. See the full roadmap in [ROADMAP.md](ROADMAP.md).
<br/>
## Community & Plugins
@@ -263,12 +284,12 @@ Paperclip collects anonymous usage telemetry to help us understand how the produ
Telemetry is **enabled by default** and can be disabled with any of the following:
| Method | How |
|---|---|
| Environment variable | `PAPERCLIP_TELEMETRY_DISABLED=1` |
| Standard convention | `DO_NOT_TRACK=1` |
| CI environments | Automatically disabled when `CI=true` |
| Config file | Set `telemetry.enabled: false` in your Paperclip config |
| Method | How |
| -------------------- | ------------------------------------------------------- |
| Environment variable | `PAPERCLIP_TELEMETRY_DISABLED=1` |
| Standard convention | `DO_NOT_TRACK=1` |
| CI environments | Automatically disabled when `CI=true` |
| Config file | Set `telemetry.enabled: false` in your Paperclip config |
## Contributing

97
ROADMAP.md Normal file
View File

@@ -0,0 +1,97 @@
# Roadmap
This document expands the roadmap preview in `README.md`.
Paperclip is still moving quickly. The list below is directional, not promised, and priorities may shift as we learn from users and from operating real AI companies with the product.
We value community involvement and want to make sure contributor energy goes toward areas where it can land.
We may accept contributions in the areas below, but if you want to work on roadmap-level core features, please coordinate with us first in Discord (`#dev`) before writing code. Bugs, docs, polish, and tightly scoped improvements are still the easiest contributions to merge.
If you want to extend Paperclip today, the best path is often the [plugin system](doc/plugins/PLUGIN_SPEC.md). Community reference implementations are also useful feedback even when they are not merged directly into core.
## Milestones
### ✅ Plugin system
Paperclip should keep a thin core and rich edges. Plugins are the path for optional capabilities like knowledge bases, custom tracing, queues, doc editors, and other product-specific surfaces that do not need to live in the control plane itself.
### ✅ Get OpenClaw / claw-style agent employees
Paperclip should be able to hire and manage real claw-style agent workers, not just a narrow built-in runtime. This is part of the larger "bring your own agent" story and keeps the control plane useful across different agent ecosystems.
### ✅ companies.sh - import and export entire organizations
Reusable companies matter. Import/export is the foundation for moving org structures, agent definitions, and reusable company setups between environments and eventually for broader company-template distribution.
### ✅ Easy AGENTS.md configurations
Agent setup should feel repo-native and legible. Simple `AGENTS.md`-style configuration lowers the barrier to getting an agent team running and makes it easier for contributors to understand how a company is wired together.
### ✅ Skills Manager
Agents need a practical way to discover, install, and use skills without every setup becoming bespoke. The skills layer is part of making Paperclip companies more reusable and easier to operate.
### ✅ Scheduled Routines
Recurring work should be native. Routine tasks like reports, reviews, and other periodic work need first-class scheduling so the company keeps operating even when no human is manually kicking work off.
### ✅ Better Budgeting
Budgets are a core control-plane feature, not an afterthought. Better budgeting means clearer spend visibility, safer hard stops, and better operator control over how autonomy turns into real cost.
### ✅ Agent Reviews and Approvals
Paperclip should support explicit review and approval stages as first-class workflow steps, not just ad hoc comments. That means reviewer routing, approval gates, change requests, and durable audit trails that fit the same task model as the rest of the control plane.
### ✅ Multiple Human Users
Paperclip needs a clearer path from solo operator to real human teams. That means shared board access, safer collaboration, and a better model for several humans supervising the same autonomous company.
### ⚪ Cloud / Sandbox agents (e.g. Cursor / e2b agents)
We want agents to run in more remote and sandboxed environments while preserving the same Paperclip control-plane model. This makes the system safer, more flexible, and more useful outside a single trusted local machine.
### ⚪ Artifacts & Work Products
Paperclip should make outputs first-class. That means generated artifacts, previews, deployable outputs, and the handoff from "agent did work" to "here is the result" should become more visible and easier to operate.
### ⚪ Memory / Knowledge
We want a stronger memory and knowledge surface for companies, agents, and projects. That includes durable memory, better recall of prior decisions and context, and a clearer path for knowledge-style capabilities without turning Paperclip into a generic chat app.
### ⚪ Enforced Outcomes
Paperclip should get stricter about what counts as finished work. Tasks, approvals, and execution flows should resolve to clear outcomes like merged code, published artifacts, shipped docs, or explicit decisions instead of stopping at vague status updates.
### ⚪ MAXIMIZER MODE
This is the direction for higher-autonomy execution: more aggressive delegation, deeper follow-through, and stronger operating loops with clear budgets, visibility, and governance. The point is not hidden autonomy; the point is more output per human supervisor.
### ⚪ Deep Planning
Some work needs more than a task description before execution starts. Deeper planning means stronger issue documents, revisionable plans, and clearer review loops for strategy-heavy work before agents begin execution.
### ⚪ Work Queues
Paperclip should support queue-style work streams for repeatable inputs like support, triage, review, and backlog intake. That would make it easier to route work continuously without turning every system into a one-off workflow.
### ⚪ Self-Organization
As companies grow, agents should be able to propose useful structural changes such as role adjustments, delegation changes, and new recurring routines. The goal is adaptive organizations that still stay within governance and approval boundaries.
### ⚪ Automatic Organizational Learning
Paperclip should get better at turning completed work into reusable organizational knowledge. That includes capturing playbooks, recurring fixes, and decision patterns so future work starts from what the company has already learned.
### ⚪ CEO Chat
We want a lighter-weight way to talk to leadership agents, but those conversations should still resolve to real work objects like plans, issues, approvals, or decisions. This should improve interaction without changing the core task-and-comments model.
### ⚪ Cloud deployments
Local-first remains important, but Paperclip also needs a cleaner shared deployment story. Teams should be able to run the same product in hosted or semi-hosted environments without changing the mental model.
### ⚪ Desktop App
A desktop app can make Paperclip feel more accessible and persistent for day-to-day operators. The goal is easier access, better local ergonomics, and a smoother default experience for users who want the control plane always close at hand.

8
SECURITY.md Normal file
View File

@@ -0,0 +1,8 @@
# Security Policy
## Reporting a Vulnerability
Please report security vulnerabilities through GitHub's Security Advisory feature:
[https://github.com/paperclipai/paperclip/security/advisories/new](https://github.com/paperclipai/paperclip/security/advisories/new)
Do not open public issues for security vulnerabilities.

View File

@@ -12,7 +12,7 @@
<p align="center">
<a href="https://github.com/paperclipai/paperclip/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" /></a>
<a href="https://github.com/paperclipai/paperclip/stargazers"><img src="https://img.shields.io/github/stars/paperclipai/paperclip?style=flat" alt="Stars" /></a>
<a href="https://discord.gg/m4HZY7xNG3"><img src="https://img.shields.io/badge/discord-join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
<a href="https://discord.gg/m4HZY7xNG3"><img src="https://img.shields.io/discord/000000000?label=discord" alt="Discord" /></a>
</p>
<br/>
@@ -177,6 +177,14 @@ Open source. Self-hosted. No Paperclip account required.
npx paperclipai onboard --yes
```
That quickstart path now defaults to trusted local loopback mode for the fastest first run. To start in authenticated/private mode instead, choose a bind preset explicitly:
```bash
npx paperclipai onboard --yes --bind lan
# or:
npx paperclipai onboard --yes --bind tailnet
```
If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to edit settings.
Or manually:
@@ -225,11 +233,15 @@ pnpm dev:once # Full dev without file watching
pnpm dev:server # Server only
pnpm build # Build all
pnpm typecheck # Type checking
pnpm test:run # Run tests
pnpm test # Cheap default test run (Vitest only)
pnpm test:watch # Vitest watch mode
pnpm test:e2e # Playwright browser suite
pnpm db:generate # Generate DB migration
pnpm db:migrate # Apply migrations
```
`pnpm test` does not run Playwright. Browser suites stay separate and are typically run only when working on those flows or in CI.
See [doc/DEVELOPING.md](https://github.com/paperclipai/paperclip/blob/master/doc/DEVELOPING.md) for the full development guide.
<br/>
@@ -246,7 +258,7 @@ See [doc/DEVELOPING.md](https://github.com/paperclipai/paperclip/blob/master/doc
- ⚪ Artifacts & Deployments
- ⚪ CEO Chat
- ⚪ MAXIMIZER MODE
- Multiple Human Users
- Multiple Human Users
- ⚪ Cloud / Sandbox agents (e.g. Cursor / e2b agents)
- ⚪ Cloud deployments
- ⚪ Desktop App

View File

@@ -287,6 +287,11 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: `CLI Export Source ${Date.now()}` }),
});
await api(apiBase, `/api/companies/${sourceCompany.id}`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ requireBoardApprovalForNewAgents: false }),
});
const sourceAgent = await api<{ id: string; name: string }>(
apiBase,

View File

@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { resolveRuntimeBind, validateConfiguredBindMode } from "@paperclipai/shared";
import { buildPresetServerConfig } from "../config/server-bind.js";
describe("network bind helpers", () => {
it("rejects non-loopback bind modes in local_trusted", () => {
expect(
validateConfiguredBindMode({
deploymentMode: "local_trusted",
deploymentExposure: "private",
bind: "lan",
host: "0.0.0.0",
}),
).toContain("local_trusted requires server.bind=loopback");
});
it("resolves tailnet bind using the detected tailscale address", () => {
const resolved = resolveRuntimeBind({
bind: "tailnet",
host: "127.0.0.1",
tailnetBindHost: "100.64.0.8",
});
expect(resolved.errors).toEqual([]);
expect(resolved.host).toBe("100.64.0.8");
});
it("requires a custom bind host when bind=custom", () => {
const resolved = resolveRuntimeBind({
bind: "custom",
host: "127.0.0.1",
});
expect(resolved.errors).toContain("server.customBindHost is required when server.bind=custom");
});
it("stores the detected tailscale address for tailnet presets", () => {
process.env.PAPERCLIP_TAILNET_BIND_HOST = "100.64.0.8";
const preset = buildPresetServerConfig("tailnet", {
port: 3100,
allowedHostnames: [],
serveUi: true,
});
expect(preset.server.host).toBe("100.64.0.8");
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
});
it("falls back to loopback when no tailscale address is available for tailnet presets", () => {
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
const preset = buildPresetServerConfig("tailnet", {
port: 3100,
allowedHostnames: [],
serveUi: true,
});
expect(preset.server.host).toBe("127.0.0.1");
});
});

View File

@@ -74,6 +74,11 @@ function createExistingConfigFixture() {
return { configPath, configText: fs.readFileSync(configPath, "utf8") };
}
function createFreshConfigPath() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-onboard-fresh-"));
return path.join(root, ".paperclip", "config.json");
}
describe("onboard", () => {
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
@@ -105,4 +110,57 @@ describe("onboard", () => {
expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false);
expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true);
});
it("keeps --yes onboarding on local trusted loopback defaults", async () => {
const configPath = createFreshConfigPath();
process.env.HOST = "0.0.0.0";
process.env.PAPERCLIP_BIND = "lan";
await onboard({ config: configPath, yes: true, invokedByRun: true });
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as PaperclipConfig;
expect(raw.server.deploymentMode).toBe("local_trusted");
expect(raw.server.exposure).toBe("private");
expect(raw.server.bind).toBe("loopback");
expect(raw.server.host).toBe("127.0.0.1");
});
it("supports authenticated/private quickstart bind presets", async () => {
const configPath = createFreshConfigPath();
process.env.PAPERCLIP_TAILNET_BIND_HOST = "100.64.0.8";
await onboard({ config: configPath, yes: true, invokedByRun: true, bind: "tailnet" });
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as PaperclipConfig;
expect(raw.server.deploymentMode).toBe("authenticated");
expect(raw.server.exposure).toBe("private");
expect(raw.server.bind).toBe("tailnet");
expect(raw.server.host).toBe("100.64.0.8");
});
it("keeps tailnet quickstart on loopback until tailscale is available", async () => {
const configPath = createFreshConfigPath();
delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
await onboard({ config: configPath, yes: true, invokedByRun: true, bind: "tailnet" });
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as PaperclipConfig;
expect(raw.server.deploymentMode).toBe("authenticated");
expect(raw.server.exposure).toBe("private");
expect(raw.server.bind).toBe("tailnet");
expect(raw.server.host).toBe("127.0.0.1");
});
it("ignores deployment env overrides during --yes quickstart", async () => {
const configPath = createFreshConfigPath();
process.env.PAPERCLIP_DEPLOYMENT_MODE = "authenticated";
await onboard({ config: configPath, yes: true, invokedByRun: true });
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as PaperclipConfig;
expect(raw.server.deploymentMode).toBe("local_trusted");
expect(raw.server.exposure).toBe("private");
expect(raw.server.bind).toBe("loopback");
expect(raw.server.host).toBe("127.0.0.1");
});
});

View File

@@ -2,10 +2,25 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
agents,
authUsers,
companies,
createDb,
issueComments,
issues,
projects,
routines,
routineTriggers,
} from "@paperclipai/db";
import {
copyGitHooksToWorktreeGitDir,
copySeededSecretsKey,
pauseSeededScheduledRoutines,
quarantineSeededWorktreeExecutionState,
readSourceAttachmentBody,
rebindWorkspaceCwd,
resolveSourceConfigPath,
@@ -13,6 +28,7 @@ import {
resolveWorktreeReseedTargetPaths,
resolveGitWorktreeAddArgs,
resolveWorktreeMakeTargetPath,
worktreeRepairCommand,
worktreeInitCommand,
worktreeMakeCommand,
worktreeReseedCommand,
@@ -28,9 +44,22 @@ import {
sanitizeWorktreeInstanceId,
} from "../commands/worktree-lib.js";
import type { PaperclipConfig } from "../config/schema.js";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
const ORIGINAL_CWD = process.cwd();
const ORIGINAL_ENV = { ...process.env };
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const itEmbeddedPostgres = embeddedPostgresSupport.supported ? it : it.skip;
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres worktree CLI tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
afterEach(() => {
process.chdir(ORIGINAL_CWD);
@@ -257,6 +286,138 @@ describe("worktree helpers", () => {
expect(full.nullifyColumns).toEqual({});
});
itEmbeddedPostgres("quarantines copied live execution state in seeded worktree databases", async () => {
const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-quarantine-");
const db = createDb(tempDb.connectionString);
const companyId = randomUUID();
const agentId = randomUUID();
const idleAgentId = randomUUID();
const inProgressIssueId = randomUUID();
const todoIssueId = randomUUID();
const reviewIssueId = randomUUID();
const userIssueId = randomUUID();
try {
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: "WTQ",
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values([
{
id: agentId,
companyId,
name: "CodexCoder",
role: "engineer",
status: "running",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {
heartbeat: { enabled: true, intervalSec: 60 },
wakeOnDemand: true,
},
permissions: {},
},
{
id: idleAgentId,
companyId,
name: "Reviewer",
role: "reviewer",
status: "idle",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: { heartbeat: { enabled: false, intervalSec: 300 } },
permissions: {},
},
]);
await db.insert(issues).values([
{
id: inProgressIssueId,
companyId,
title: "Copied in-flight issue",
status: "in_progress",
priority: "medium",
assigneeAgentId: agentId,
issueNumber: 1,
identifier: "WTQ-1",
executionAgentNameKey: "codexcoder",
executionLockedAt: new Date("2026-04-18T00:00:00.000Z"),
},
{
id: todoIssueId,
companyId,
title: "Copied assigned todo issue",
status: "todo",
priority: "medium",
assigneeAgentId: agentId,
issueNumber: 2,
identifier: "WTQ-2",
},
{
id: reviewIssueId,
companyId,
title: "Copied assigned review issue",
status: "in_review",
priority: "medium",
assigneeAgentId: idleAgentId,
issueNumber: 3,
identifier: "WTQ-3",
},
{
id: userIssueId,
companyId,
title: "Copied user issue",
status: "todo",
priority: "medium",
assigneeUserId: "user-1",
issueNumber: 4,
identifier: "WTQ-4",
},
]);
await expect(quarantineSeededWorktreeExecutionState(tempDb.connectionString)).resolves.toEqual({
disabledTimerHeartbeats: 1,
resetRunningAgents: 1,
quarantinedInProgressIssues: 1,
unassignedTodoIssues: 1,
unassignedReviewIssues: 1,
});
const [quarantinedAgent] = await db.select().from(agents).where(eq(agents.id, agentId));
expect(quarantinedAgent?.status).toBe("idle");
expect(quarantinedAgent?.runtimeConfig).toMatchObject({
heartbeat: { enabled: false, intervalSec: 60 },
wakeOnDemand: true,
});
const [inProgressIssue] = await db.select().from(issues).where(eq(issues.id, inProgressIssueId));
expect(inProgressIssue?.status).toBe("blocked");
expect(inProgressIssue?.assigneeAgentId).toBeNull();
expect(inProgressIssue?.executionAgentNameKey).toBeNull();
expect(inProgressIssue?.executionLockedAt).toBeNull();
const [todoIssue] = await db.select().from(issues).where(eq(issues.id, todoIssueId));
expect(todoIssue?.status).toBe("todo");
expect(todoIssue?.assigneeAgentId).toBeNull();
const [reviewIssue] = await db.select().from(issues).where(eq(issues.id, reviewIssueId));
expect(reviewIssue?.status).toBe("in_review");
expect(reviewIssue?.assigneeAgentId).toBeNull();
const [userIssue] = await db.select().from(issues).where(eq(issues.id, userIssueId));
expect(userIssue?.status).toBe("todo");
expect(userIssue?.assigneeUserId).toBe("user-1");
const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, inProgressIssueId));
expect(comments).toHaveLength(1);
expect(comments[0]?.body).toContain("Quarantined during worktree seed");
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
await tempDb.cleanup();
}
}, 20_000);
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
const originalInlineMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
@@ -350,6 +511,97 @@ describe("worktree helpers", () => {
}
});
itEmbeddedPostgres(
"seeds authenticated users into minimally cloned worktree instances",
async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-auth-seed-"));
const worktreeRoot = path.join(tempRoot, "PAP-999-auth-seed");
const sourceHome = path.join(tempRoot, "source-home");
const sourceConfigDir = path.join(sourceHome, "instances", "source");
const sourceConfigPath = path.join(sourceConfigDir, "config.json");
const sourceEnvPath = path.join(sourceConfigDir, ".env");
const sourceKeyPath = path.join(sourceConfigDir, "secrets", "master.key");
const worktreeHome = path.join(tempRoot, ".paperclip-worktrees");
const originalCwd = process.cwd();
const sourceDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-auth-source-");
try {
const sourceDbClient = createDb(sourceDb.connectionString);
await sourceDbClient.insert(authUsers).values({
id: "user-existing",
email: "existing@paperclip.ing",
name: "Existing User",
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
});
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
fs.mkdirSync(worktreeRoot, { recursive: true });
const sourceConfig = buildSourceConfig();
sourceConfig.database = {
mode: "postgres",
embeddedPostgresDataDir: path.join(sourceConfigDir, "db"),
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(sourceConfigDir, "backups"),
},
connectionString: sourceDb.connectionString,
};
sourceConfig.logging.logDir = path.join(sourceConfigDir, "logs");
sourceConfig.storage.localDisk.baseDir = path.join(sourceConfigDir, "storage");
sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath;
fs.writeFileSync(sourceConfigPath, JSON.stringify(sourceConfig, null, 2) + "\n", "utf8");
fs.writeFileSync(sourceEnvPath, "", "utf8");
fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8");
process.chdir(worktreeRoot);
await worktreeInitCommand({
name: "PAP-999-auth-seed",
home: worktreeHome,
fromConfig: sourceConfigPath,
force: true,
});
const targetConfig = JSON.parse(
fs.readFileSync(path.join(worktreeRoot, ".paperclip", "config.json"), "utf8"),
) as PaperclipConfig;
const { default: EmbeddedPostgres } = await import("embedded-postgres");
const targetPg = new EmbeddedPostgres({
databaseDir: targetConfig.database.embeddedPostgresDataDir,
user: "paperclip",
password: "paperclip",
port: targetConfig.database.embeddedPostgresPort,
persistent: true,
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
onLog: () => {},
onError: () => {},
});
await targetPg.start();
try {
const targetDb = createDb(
`postgres://paperclip:paperclip@127.0.0.1:${targetConfig.database.embeddedPostgresPort}/paperclip`,
);
const seededUsers = await targetDb.select().from(authUsers);
expect(seededUsers.some((row) => row.email === "existing@paperclip.ing")).toBe(true);
} finally {
await targetPg.stop();
}
} finally {
process.chdir(originalCwd);
await sourceDb.cleanup();
fs.rmSync(tempRoot, { recursive: true, force: true });
}
},
20000,
);
it("avoids ports already claimed by sibling worktree instance configs", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-claimed-ports-"));
const repoRoot = path.join(tempRoot, "repo");
@@ -822,4 +1074,246 @@ describe("worktree helpers", () => {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}, 20_000);
it("no-ops on the primary checkout unless --branch is provided", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-primary-"));
const repoRoot = path.join(tempRoot, "repo");
const originalCwd = process.cwd();
try {
fs.mkdirSync(repoRoot, { recursive: true });
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
process.chdir(repoRoot);
await worktreeRepairCommand({});
expect(fs.existsSync(path.join(repoRoot, ".paperclip", "config.json"))).toBe(false);
expect(fs.existsSync(path.join(repoRoot, ".paperclip", "worktrees"))).toBe(false);
} finally {
process.chdir(originalCwd);
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it("repairs the current linked worktree when Paperclip metadata is missing", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-current-"));
const repoRoot = path.join(tempRoot, "repo");
const worktreePath = path.join(repoRoot, ".paperclip", "worktrees", "repair-me");
const sourceConfigPath = path.join(tempRoot, "source-config.json");
const worktreeHome = path.join(tempRoot, ".paperclip-worktrees");
const worktreePaths = resolveWorktreeLocalPaths({
cwd: worktreePath,
homeDir: worktreeHome,
instanceId: sanitizeWorktreeInstanceId(path.basename(worktreePath)),
});
const originalCwd = process.cwd();
try {
fs.mkdirSync(repoRoot, { recursive: true });
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
execFileSync("git", ["worktree", "add", "-b", "repair-me", worktreePath, "HEAD"], {
cwd: repoRoot,
stdio: "ignore",
});
fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig(), null, 2), "utf8");
fs.mkdirSync(worktreePaths.instanceRoot, { recursive: true });
fs.writeFileSync(path.join(worktreePaths.instanceRoot, "marker.txt"), "stale", "utf8");
process.chdir(worktreePath);
await worktreeRepairCommand({
fromConfig: sourceConfigPath,
home: worktreeHome,
noSeed: true,
});
expect(fs.existsSync(path.join(worktreePath, ".paperclip", "config.json"))).toBe(true);
expect(fs.existsSync(path.join(worktreePath, ".paperclip", ".env"))).toBe(true);
expect(fs.existsSync(path.join(worktreePaths.instanceRoot, "marker.txt"))).toBe(false);
} finally {
process.chdir(originalCwd);
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}, 20_000);
it("creates and repairs a missing branch worktree when --branch is provided", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-branch-"));
const repoRoot = path.join(tempRoot, "repo");
const sourceConfigPath = path.join(tempRoot, "source-config.json");
const worktreeHome = path.join(tempRoot, ".paperclip-worktrees");
const originalCwd = process.cwd();
const expectedWorktreePath = path.join(repoRoot, ".paperclip", "worktrees", "feature-repair-me");
try {
fs.mkdirSync(repoRoot, { recursive: true });
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" });
fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8");
execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" });
execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" });
fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig(), null, 2), "utf8");
process.chdir(repoRoot);
await worktreeRepairCommand({
branch: "feature/repair-me",
fromConfig: sourceConfigPath,
home: worktreeHome,
noSeed: true,
});
expect(fs.existsSync(path.join(expectedWorktreePath, ".git"))).toBe(true);
expect(fs.existsSync(path.join(expectedWorktreePath, ".paperclip", "config.json"))).toBe(true);
expect(fs.existsSync(path.join(expectedWorktreePath, ".paperclip", ".env"))).toBe(true);
} finally {
process.chdir(originalCwd);
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}, 20_000);
});
describeEmbeddedPostgres("pauseSeededScheduledRoutines", () => {
it("pauses only routines with enabled schedule triggers", async () => {
const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-routines-");
const db = createDb(tempDb.connectionString);
const companyId = randomUUID();
const projectId = randomUUID();
const agentId = randomUUID();
const activeScheduledRoutineId = randomUUID();
const activeApiRoutineId = randomUUID();
const pausedScheduledRoutineId = randomUUID();
const archivedScheduledRoutineId = randomUUID();
const disabledScheduleRoutineId = randomUUID();
try {
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "Coder",
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
});
await db.insert(projects).values({
id: projectId,
companyId,
name: "Project",
status: "in_progress",
});
await db.insert(routines).values([
{
id: activeScheduledRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Active scheduled",
status: "active",
},
{
id: activeApiRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Active API",
status: "active",
},
{
id: pausedScheduledRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Paused scheduled",
status: "paused",
},
{
id: archivedScheduledRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Archived scheduled",
status: "archived",
},
{
id: disabledScheduleRoutineId,
companyId,
projectId,
assigneeAgentId: agentId,
title: "Disabled schedule",
status: "active",
},
]);
await db.insert(routineTriggers).values([
{
companyId,
routineId: activeScheduledRoutineId,
kind: "schedule",
enabled: true,
cronExpression: "0 9 * * *",
timezone: "UTC",
},
{
companyId,
routineId: activeApiRoutineId,
kind: "api",
enabled: true,
},
{
companyId,
routineId: pausedScheduledRoutineId,
kind: "schedule",
enabled: true,
cronExpression: "0 10 * * *",
timezone: "UTC",
},
{
companyId,
routineId: archivedScheduledRoutineId,
kind: "schedule",
enabled: true,
cronExpression: "0 11 * * *",
timezone: "UTC",
},
{
companyId,
routineId: disabledScheduleRoutineId,
kind: "schedule",
enabled: false,
cronExpression: "0 12 * * *",
timezone: "UTC",
},
]);
const pausedCount = await pauseSeededScheduledRoutines(tempDb.connectionString);
expect(pausedCount).toBe(1);
const rows = await db.select({ id: routines.id, status: routines.status }).from(routines);
const statusById = new Map(rows.map((row) => [row.id, row.status]));
expect(statusById.get(activeScheduledRoutineId)).toBe("paused");
expect(statusById.get(activeApiRoutineId)).toBe("active");
expect(statusById.get(pausedScheduledRoutineId)).toBe("paused");
expect(statusById.get(archivedScheduledRoutineId)).toBe("archived");
expect(statusById.get(disabledScheduleRoutineId)).toBe("active");
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
await tempDb.cleanup();
}
}, 20_000);
});

View File

@@ -1,24 +1,21 @@
import { inferBindModeFromHost } from "@paperclipai/shared";
import type { PaperclipConfig } from "../config/schema.js";
import type { CheckResult } from "./index.js";
function isLoopbackHost(host: string) {
const normalized = host.trim().toLowerCase();
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1";
}
export function deploymentAuthCheck(config: PaperclipConfig): CheckResult {
const mode = config.server.deploymentMode;
const exposure = config.server.exposure;
const auth = config.auth;
const bind = config.server.bind ?? inferBindModeFromHost(config.server.host);
if (mode === "local_trusted") {
if (!isLoopbackHost(config.server.host)) {
if (bind !== "loopback") {
return {
name: "Deployment/auth mode",
status: "fail",
message: `local_trusted requires loopback host binding (found ${config.server.host})`,
message: `local_trusted requires loopback binding (found ${bind})`,
canRepair: false,
repairHint: "Run `paperclipai configure --section server` and set host to 127.0.0.1",
repairHint: "Run `paperclipai configure --section server` and choose Local trusted / loopback reachability",
};
}
return {
@@ -86,6 +83,6 @@ export function deploymentAuthCheck(config: PaperclipConfig): CheckResult {
return {
name: "Deployment/auth mode",
status: "pass",
message: `Mode ${mode}/${exposure} with auth URL mode ${auth.baseUrlMode}`,
message: `Mode ${mode}/${exposure} with bind ${bind} and auth URL mode ${auth.baseUrlMode}`,
};
}

View File

@@ -3,6 +3,7 @@ import * as p from "@clack/prompts";
import pc from "picocolors";
import { and, eq, gt, isNull } from "drizzle-orm";
import { createDb, instanceUserRoles, invites } from "@paperclipai/db";
import { inferBindModeFromHost } from "@paperclipai/shared";
import { loadPaperclipEnvFile } from "../config/env.js";
import { readConfig, resolveConfigPath } from "../config/store.js";
@@ -40,9 +41,13 @@ function resolveBaseUrl(configPath?: string, explicitBaseUrl?: string) {
if (config?.auth.baseUrlMode === "explicit" && config.auth.publicBaseUrl) {
return config.auth.publicBaseUrl.replace(/\/+$/, "");
}
const host = config?.server.host ?? "localhost";
const bind = config?.server.bind ?? inferBindModeFromHost(config?.server.host);
const host =
bind === "custom"
? config?.server.customBindHost ?? config?.server.host ?? "localhost"
: config?.server.host ?? "localhost";
const port = config?.server.port ?? 3100;
const publicHost = host === "0.0.0.0" ? "localhost" : host;
const publicHost = host === "0.0.0.0" || bind === "lan" ? "localhost" : host;
return `http://${publicHost}:${port}`;
}

View File

@@ -54,6 +54,7 @@ function defaultConfig(): PaperclipConfig {
server: {
deploymentMode: "local_trusted",
exposure: "private",
bind: "loopback",
host: "127.0.0.1",
port: 3100,
allowedHostnames: [],

View File

@@ -73,7 +73,7 @@ export async function dbBackupCommand(opts: DbBackupOptions): Promise<void> {
const result = await runDatabaseBackup({
connectionString: connection.value,
backupDir,
retentionDays,
retention: { dailyDays: retentionDays, weeklyWeeks: 4, monthlyMonths: 1 },
filenamePrefix,
});
spinner.stop(`Backup saved: ${formatDatabaseBackupResult(result)}`);

View File

@@ -3,10 +3,14 @@ import path from "node:path";
import pc from "picocolors";
import {
AUTH_BASE_URL_MODES,
BIND_MODES,
DEPLOYMENT_EXPOSURES,
DEPLOYMENT_MODES,
SECRET_PROVIDERS,
STORAGE_PROVIDERS,
inferBindModeFromHost,
resolveRuntimeBind,
type BindMode,
type AuthBaseUrlMode,
type DeploymentExposure,
type DeploymentMode,
@@ -23,6 +27,7 @@ import { promptLogging } from "../prompts/logging.js";
import { defaultSecretsConfig } from "../prompts/secrets.js";
import { defaultStorageConfig, promptStorage } from "../prompts/storage.js";
import { promptServer } from "../prompts/server.js";
import { buildPresetServerConfig } from "../config/server-bind.js";
import {
describeLocalInstancePaths,
expandHomePrefix,
@@ -46,10 +51,14 @@ type OnboardOptions = {
run?: boolean;
yes?: boolean;
invokedByRun?: boolean;
bind?: BindMode;
};
type OnboardDefaults = Pick<PaperclipConfig, "database" | "logging" | "server" | "auth" | "storage" | "secrets">;
const TAILNET_BIND_WARNING =
"No Tailscale address was detected during setup. The saved config will stay on loopback until Tailscale is available or PAPERCLIP_TAILNET_BIND_HOST is set.";
const ONBOARD_ENV_KEYS = [
"PAPERCLIP_PUBLIC_URL",
"DATABASE_URL",
@@ -59,6 +68,9 @@ const ONBOARD_ENV_KEYS = [
"PAPERCLIP_DB_BACKUP_DIR",
"PAPERCLIP_DEPLOYMENT_MODE",
"PAPERCLIP_DEPLOYMENT_EXPOSURE",
"PAPERCLIP_BIND",
"PAPERCLIP_BIND_HOST",
"PAPERCLIP_TAILNET_BIND_HOST",
"HOST",
"PORT",
"SERVE_UI",
@@ -104,29 +116,62 @@ function resolvePathFromEnv(rawValue: string | undefined): string | null {
return path.resolve(expandHomePrefix(rawValue.trim()));
}
function quickstartDefaultsFromEnv(): {
function describeServerBinding(server: Pick<PaperclipConfig["server"], "bind" | "customBindHost" | "host" | "port">): string {
const bind = server.bind ?? inferBindModeFromHost(server.host);
const detail =
bind === "custom"
? server.customBindHost ?? server.host
: bind === "tailnet"
? "detected tailscale address"
: server.host;
return `${bind}${detail ? ` (${detail})` : ""}:${server.port}`;
}
function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): {
defaults: OnboardDefaults;
usedEnvKeys: string[];
ignoredEnvKeys: Array<{ key: string; reason: string }>;
} {
const preferTrustedLocal = opts?.preferTrustedLocal ?? false;
const instanceId = resolvePaperclipInstanceId();
const defaultStorage = defaultStorageConfig();
const defaultSecrets = defaultSecretsConfig();
const databaseUrl = process.env.DATABASE_URL?.trim() || undefined;
const publicUrl =
process.env.PAPERCLIP_PUBLIC_URL?.trim() ||
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL?.trim() ||
process.env.BETTER_AUTH_URL?.trim() ||
process.env.BETTER_AUTH_BASE_URL?.trim() ||
undefined;
const deploymentMode =
parseEnumFromEnv<DeploymentMode>(process.env.PAPERCLIP_DEPLOYMENT_MODE, DEPLOYMENT_MODES) ?? "local_trusted";
const publicUrl = preferTrustedLocal
? undefined
: (
process.env.PAPERCLIP_PUBLIC_URL?.trim() ||
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL?.trim() ||
process.env.BETTER_AUTH_URL?.trim() ||
process.env.BETTER_AUTH_BASE_URL?.trim() ||
undefined
);
const deploymentMode = preferTrustedLocal
? "local_trusted"
: (parseEnumFromEnv<DeploymentMode>(process.env.PAPERCLIP_DEPLOYMENT_MODE, DEPLOYMENT_MODES) ?? "local_trusted");
const deploymentExposureFromEnv = parseEnumFromEnv<DeploymentExposure>(
process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE,
DEPLOYMENT_EXPOSURES,
);
const deploymentExposure =
deploymentMode === "local_trusted" ? "private" : (deploymentExposureFromEnv ?? "private");
const bindFromEnv = parseEnumFromEnv<BindMode>(process.env.PAPERCLIP_BIND, BIND_MODES);
const customBindHostFromEnv = process.env.PAPERCLIP_BIND_HOST?.trim() || undefined;
const hostFromEnv = process.env.HOST?.trim() || undefined;
const configuredBindHost = customBindHostFromEnv ?? hostFromEnv;
const bind = preferTrustedLocal
? "loopback"
: (
deploymentMode === "local_trusted"
? "loopback"
: (bindFromEnv ?? (configuredBindHost ? inferBindModeFromHost(configuredBindHost) : "lan"))
);
const resolvedBind = resolveRuntimeBind({
bind,
host: hostFromEnv ?? (bind === "loopback" ? "127.0.0.1" : "0.0.0.0"),
customBindHost: customBindHostFromEnv,
tailnetBindHost: process.env.PAPERCLIP_TAILNET_BIND_HOST?.trim(),
});
const authPublicBaseUrl = publicUrl;
const authBaseUrlModeFromEnv = parseEnumFromEnv<AuthBaseUrlMode>(
process.env.PAPERCLIP_AUTH_BASE_URL_MODE,
@@ -183,7 +228,9 @@ function quickstartDefaultsFromEnv(): {
server: {
deploymentMode,
exposure: deploymentExposure,
host: process.env.HOST ?? "127.0.0.1",
bind: resolvedBind.bind,
...(resolvedBind.customBindHost ? { customBindHost: resolvedBind.customBindHost } : {}),
host: resolvedBind.host,
port: Number(process.env.PORT) || 3100,
allowedHostnames: Array.from(new Set([...allowedHostnamesFromEnv, ...(hostnameFromPublicUrl ? [hostnameFromPublicUrl] : [])])),
serveUi: parseBooleanFromEnv(process.env.SERVE_UI) ?? true,
@@ -220,12 +267,49 @@ function quickstartDefaultsFromEnv(): {
},
};
const ignoredEnvKeys: Array<{ key: string; reason: string }> = [];
if (preferTrustedLocal) {
const forcedLocalReason = "Ignored because --yes quickstart forces trusted local loopback defaults";
for (const key of [
"PAPERCLIP_DEPLOYMENT_MODE",
"PAPERCLIP_DEPLOYMENT_EXPOSURE",
"PAPERCLIP_BIND",
"PAPERCLIP_BIND_HOST",
"HOST",
"PAPERCLIP_AUTH_BASE_URL_MODE",
"PAPERCLIP_AUTH_PUBLIC_BASE_URL",
"PAPERCLIP_PUBLIC_URL",
"BETTER_AUTH_URL",
"BETTER_AUTH_BASE_URL",
] as const) {
if (process.env[key] !== undefined) {
ignoredEnvKeys.push({ key, reason: forcedLocalReason });
}
}
}
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE !== undefined) {
ignoredEnvKeys.push({
key: "PAPERCLIP_DEPLOYMENT_EXPOSURE",
reason: "Ignored because deployment mode local_trusted always forces private exposure",
});
}
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_BIND !== undefined) {
ignoredEnvKeys.push({
key: "PAPERCLIP_BIND",
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
});
}
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_BIND_HOST !== undefined) {
ignoredEnvKeys.push({
key: "PAPERCLIP_BIND_HOST",
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
});
}
if (deploymentMode === "local_trusted" && process.env.HOST !== undefined) {
ignoredEnvKeys.push({
key: "HOST",
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
});
}
const ignoredKeySet = new Set(ignoredEnvKeys.map((entry) => entry.key));
const usedEnvKeys = ONBOARD_ENV_KEYS.filter(
@@ -239,6 +323,10 @@ function canCreateBootstrapInviteImmediately(config: Pick<PaperclipConfig, "data
}
export async function onboard(opts: OnboardOptions): Promise<void> {
if (opts.bind && !["loopback", "lan", "tailnet"].includes(opts.bind)) {
throw new Error(`Unsupported bind preset for onboard: ${opts.bind}. Use loopback, lan, or tailnet.`);
}
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclipai onboard ")));
const configPath = resolveConfigPath(opts.config);
@@ -293,7 +381,7 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
`Database: ${existingConfig.database.mode}`,
existingConfig.llm ? `LLM: ${existingConfig.llm.provider}` : "LLM: not configured",
`Logging: ${existingConfig.logging.mode} -> ${existingConfig.logging.logDir}`,
`Server: ${existingConfig.server.deploymentMode}/${existingConfig.server.exposure} @ ${existingConfig.server.host}:${existingConfig.server.port}`,
`Server: ${existingConfig.server.deploymentMode}/${existingConfig.server.exposure} @ ${describeServerBinding(existingConfig.server)}`,
`Allowed hosts: ${existingConfig.server.allowedHostnames.length > 0 ? existingConfig.server.allowedHostnames.join(", ") : "(loopback only)"}`,
`Auth URL mode: ${existingConfig.auth.baseUrlMode}${existingConfig.auth.publicBaseUrl ? ` (${existingConfig.auth.publicBaseUrl})` : ""}`,
`Storage: ${existingConfig.storage.provider}`,
@@ -336,7 +424,13 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
let setupMode: SetupMode = "quickstart";
if (opts.yes) {
p.log.message(pc.dim("`--yes` enabled: using Quickstart defaults."));
p.log.message(
pc.dim(
opts.bind
? `\`--yes\` enabled: using Quickstart defaults with bind=${opts.bind}.`
: "`--yes` enabled: using Quickstart defaults.",
),
);
} else {
const setupModeChoice = await p.select({
message: "Choose setup path",
@@ -365,7 +459,9 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
if (tc) trackInstallStarted(tc);
let llm: PaperclipConfig["llm"] | undefined;
const { defaults: derivedDefaults, usedEnvKeys, ignoredEnvKeys } = quickstartDefaultsFromEnv();
const { defaults: derivedDefaults, usedEnvKeys, ignoredEnvKeys } = quickstartDefaultsFromEnv({
preferTrustedLocal: opts.yes === true && !opts.bind,
});
let {
database,
logging,
@@ -375,6 +471,19 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
secrets,
} = derivedDefaults;
if (opts.bind === "loopback" || opts.bind === "lan" || opts.bind === "tailnet") {
const preset = buildPresetServerConfig(opts.bind, {
port: server.port,
allowedHostnames: server.allowedHostnames,
serveUi: server.serveUi,
});
server = preset.server;
auth = preset.auth;
if (opts.bind === "tailnet" && server.host === "127.0.0.1") {
p.log.warn(TAILNET_BIND_WARNING);
}
}
if (setupMode === "advanced") {
p.log.step(pc.bold("Database"));
database = await promptDatabase(database);
@@ -462,7 +571,13 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
);
} else {
p.log.step(pc.bold("Quickstart"));
p.log.message(pc.dim("Using quickstart defaults."));
p.log.message(
pc.dim(
opts.bind
? `Using quickstart defaults with bind=${opts.bind}.`
: `Using quickstart defaults: ${server.deploymentMode}/${server.exposure} @ ${describeServerBinding(server)}.`,
),
);
if (usedEnvKeys.length > 0) {
p.log.message(pc.dim(`Environment-aware defaults active (${usedEnvKeys.length} env var(s) detected).`));
} else {
@@ -521,7 +636,7 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
`Database: ${database.mode}`,
llm ? `LLM: ${llm.provider}` : "LLM: not configured",
`Logging: ${logging.mode} -> ${logging.logDir}`,
`Server: ${server.deploymentMode}/${server.exposure} @ ${server.host}:${server.port}`,
`Server: ${server.deploymentMode}/${server.exposure} @ ${describeServerBinding(server)}`,
`Allowed hosts: ${server.allowedHostnames.length > 0 ? server.allowedHostnames.join(", ") : "(loopback only)"}`,
`Auth URL mode: ${auth.baseUrlMode}${auth.publicBaseUrl ? ` (${auth.publicBaseUrl})` : ""}`,
`Storage: ${storage.provider}`,

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath, pathToFileURL } from "node:url";
import * as p from "@clack/prompts";
import pc from "picocolors";
@@ -21,6 +22,7 @@ interface RunOptions {
instance?: string;
repair?: boolean;
yes?: boolean;
bind?: "loopback" | "lan" | "tailnet";
}
interface StartedServer {
@@ -57,7 +59,7 @@ export async function runCommand(opts: RunOptions): Promise<void> {
}
p.log.step("No config found. Starting onboarding...");
await onboard({ config: configPath, invokedByRun: true });
await onboard({ config: configPath, invokedByRun: true, bind: opts.bind });
}
p.log.step("Running doctor checks...");
@@ -146,11 +148,35 @@ function maybeEnableUiDevMiddleware(entrypoint: string): void {
}
}
function ensureDevWorkspaceBuildDeps(projectRoot: string): void {
const buildScript = path.resolve(projectRoot, "scripts/ensure-plugin-build-deps.mjs");
if (!fs.existsSync(buildScript)) return;
const result = spawnSync(process.execPath, [buildScript], {
cwd: projectRoot,
stdio: "inherit",
timeout: 120_000,
});
if (result.error) {
throw new Error(
`Failed to prepare workspace build artifacts before starting the Paperclip dev server.\n${formatError(result.error)}`,
);
}
if ((result.status ?? 1) !== 0) {
throw new Error(
"Failed to prepare workspace build artifacts before starting the Paperclip dev server.",
);
}
}
async function importServerEntry(): Promise<StartedServer> {
// Dev mode: try local workspace path (monorepo with tsx)
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const devEntry = path.resolve(projectRoot, "server/src/index.ts");
if (fs.existsSync(devEntry)) {
ensureDevWorkspaceBuildDeps(projectRoot);
maybeEnableUiDevMiddleware(devEntry);
const mod = await import(pathToFileURL(devEntry).href);
return await startServerFromModule(mod, devEntry);

View File

@@ -214,6 +214,8 @@ export function buildWorktreeConfig(input: {
server: {
deploymentMode: source?.server.deploymentMode ?? "local_trusted",
exposure: source?.server.exposure ?? "private",
...(source?.server.bind ? { bind: source.server.bind } : {}),
...(source?.server.customBindHost ? { customBindHost: source.server.customBindHost } : {}),
host: source?.server.host ?? "127.0.0.1",
port: serverPort,
allowedHostnames: source?.server.allowedHostnames ?? [],

View File

@@ -39,6 +39,8 @@ import {
issues,
projectWorkspaces,
projects,
routines,
routineTriggers,
runDatabaseBackup,
runDatabaseRestore,
createEmbeddedPostgresLogBuffer,
@@ -91,6 +93,7 @@ type WorktreeInitOptions = {
dbPort?: number;
seed?: boolean;
seedMode?: string;
preserveLiveWork?: boolean;
force?: boolean;
};
@@ -124,10 +127,23 @@ type WorktreeReseedOptions = {
fromDataDir?: string;
fromInstance?: string;
seedMode?: string;
preserveLiveWork?: boolean;
yes?: boolean;
allowLiveTarget?: boolean;
};
type WorktreeRepairOptions = {
branch?: string;
home?: string;
fromConfig?: string;
fromDataDir?: string;
fromInstance?: string;
seedMode?: string;
preserveLiveWork?: boolean;
noSeed?: boolean;
allowLiveTarget?: boolean;
};
type EmbeddedPostgresInstance = {
initialise(): Promise<void>;
start(): Promise<void>;
@@ -166,6 +182,8 @@ type CopiedGitHooksResult = {
type SeedWorktreeDatabaseResult = {
backupSummary: string;
pausedScheduledRoutines: number;
executionQuarantine: SeededWorktreeExecutionQuarantineSummary;
reboundWorkspaces: Array<{
name: string;
fromCwd: string;
@@ -173,6 +191,14 @@ type SeedWorktreeDatabaseResult = {
}>;
};
export type SeededWorktreeExecutionQuarantineSummary = {
disabledTimerHeartbeats: number;
resetRunningAgents: number;
quarantinedInProgressIssues: number;
unassignedTodoIssues: number;
unassignedReviewIssues: number;
};
function nonEmpty(value: string | null | undefined): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
@@ -185,6 +211,18 @@ function isCurrentSourceConfigPath(sourceConfigPath: string): boolean {
return path.resolve(currentConfigPath) === path.resolve(sourceConfigPath);
}
function formatSeededWorktreeExecutionQuarantineSummary(
summary: SeededWorktreeExecutionQuarantineSummary,
): string {
return [
`disabled timer heartbeats: ${summary.disabledTimerHeartbeats}`,
`reset running agents: ${summary.resetRunningAgents}`,
`quarantined in-progress issues: ${summary.quarantinedInProgressIssues}`,
`unassigned todo issues: ${summary.unassignedTodoIssues}`,
`unassigned review issues: ${summary.unassignedReviewIssues}`,
].join(", ");
}
const WORKTREE_NAME_PREFIX = "paperclip-";
function resolveWorktreeMakeName(name: string): string {
@@ -548,6 +586,46 @@ function detectGitBranchName(cwd: string): string | null {
}
}
function validateGitBranchName(cwd: string, branchName: string): string {
const value = nonEmpty(branchName);
if (!value) {
throw new Error("Branch name is required.");
}
try {
execFileSync("git", ["check-ref-format", "--branch", value], {
cwd,
stdio: ["ignore", "pipe", "pipe"],
});
} catch (error) {
throw new Error(`Invalid branch name "${branchName}": ${extractExecSyncErrorMessage(error) ?? String(error)}`);
}
return value;
}
function isPrimaryGitWorktree(cwd: string): boolean {
const workspace = detectGitWorkspaceInfo(cwd);
return Boolean(workspace && workspace.gitDir === workspace.commonDir);
}
function resolvePrimaryGitRepoRoot(cwd: string): string {
const workspace = detectGitWorkspaceInfo(cwd);
if (!workspace) {
throw new Error("Current directory is not inside a git repository.");
}
if (workspace.gitDir === workspace.commonDir) {
return workspace.root;
}
return path.resolve(workspace.commonDir, "..");
}
function resolveRepairWorktreeDirName(branchName: string): string {
const normalized = branchName.trim()
.replace(/[^A-Za-z0-9._-]+/g, "-")
.replace(/-+/g, "-")
.replace(/^[-._]+|[-._]+$/g, "");
return normalized || "worktree";
}
function detectGitWorkspaceInfo(cwd: string): GitWorkspaceInfo | null {
try {
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
@@ -771,6 +849,21 @@ export function resolveWorktreeReseedSource(input: WorktreeReseedOptions): Resol
);
}
function resolveWorktreeRepairSource(input: WorktreeRepairOptions): ResolvedWorktreeReseedSource {
const fromConfig = nonEmpty(input.fromConfig);
const fromDataDir = nonEmpty(input.fromDataDir);
const fromInstance = nonEmpty(input.fromInstance) ?? "default";
const configPath = resolveSourceConfigPath({
fromConfig: fromConfig ?? undefined,
fromDataDir: fromDataDir ?? undefined,
fromInstance,
});
return {
configPath,
label: configPath,
};
}
export function resolveWorktreeReseedTargetPaths(input: {
configPath: string;
rootPath: string;
@@ -792,6 +885,105 @@ export function resolveWorktreeReseedTargetPaths(input: {
});
}
function resolveExistingGitWorktree(selector: string, cwd: string): MergeSourceChoice | null {
const trimmed = selector.trim();
if (trimmed.length === 0) return null;
const directPath = path.resolve(trimmed);
if (existsSync(directPath)) {
return {
worktree: directPath,
branch: null,
branchLabel: path.basename(directPath),
hasPaperclipConfig: existsSync(path.resolve(directPath, ".paperclip", "config.json")),
isCurrent: directPath === path.resolve(cwd),
};
}
return toMergeSourceChoices(cwd).find((choice) =>
choice.worktree === directPath
|| path.basename(choice.worktree) === trimmed
|| choice.branchLabel === trimmed
|| choice.branch === trimmed,
) ?? null;
}
async function ensureRepairTargetWorktree(input: {
selector?: string;
seedMode: WorktreeSeedMode;
opts: WorktreeRepairOptions;
}): Promise<ResolvedWorktreeRepairTarget | null> {
const cwd = process.cwd();
const currentRoot = path.resolve(cwd);
const currentConfigPath = path.resolve(currentRoot, ".paperclip", "config.json");
if (!input.selector) {
if (isPrimaryGitWorktree(cwd)) {
return null;
}
return {
rootPath: currentRoot,
configPath: currentConfigPath,
label: path.basename(currentRoot),
branchName: detectGitBranchName(cwd),
created: false,
};
}
const existing = resolveExistingGitWorktree(input.selector, cwd);
if (existing) {
return {
rootPath: existing.worktree,
configPath: path.resolve(existing.worktree, ".paperclip", "config.json"),
label: existing.branchLabel,
branchName: existing.branchLabel === "(detached)" ? null : existing.branchLabel,
created: false,
};
}
const repoRoot = resolvePrimaryGitRepoRoot(cwd);
const branchName = validateGitBranchName(repoRoot, input.selector);
const targetPath = path.resolve(
repoRoot,
".paperclip",
"worktrees",
resolveRepairWorktreeDirName(branchName),
);
if (existsSync(targetPath)) {
throw new Error(`Target path already exists but is not a registered git worktree: ${targetPath}`);
}
mkdirSync(path.dirname(targetPath), { recursive: true });
const spinner = p.spinner();
spinner.start(`Creating git worktree for ${branchName}...`);
try {
execFileSync("git", resolveGitWorktreeAddArgs({
branchName,
targetPath,
branchExists: localBranchExists(repoRoot, branchName),
}), {
cwd: repoRoot,
stdio: ["ignore", "pipe", "pipe"],
});
spinner.stop(`Created git worktree at ${targetPath}.`);
} catch (error) {
spinner.stop(pc.red("Failed to create git worktree."));
throw new Error(extractExecSyncErrorMessage(error) ?? String(error));
}
installDependenciesBestEffort(targetPath);
return {
rootPath: targetPath,
configPath: path.resolve(targetPath, ".paperclip", "config.json"),
label: branchName,
branchName,
created: true,
};
}
function resolveSourceConnectionString(config: PaperclipConfig, envEntries: Record<string, string>, portOverride?: number): string {
if (config.database.mode === "postgres") {
const connectionString = nonEmpty(envEntries.DATABASE_URL) ?? nonEmpty(config.database.connectionString);
@@ -922,6 +1114,163 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P
};
}
export async function pauseSeededScheduledRoutines(connectionString: string): Promise<number> {
const db = createDb(connectionString);
try {
const scheduledRoutineIds = await db
.selectDistinct({ routineId: routineTriggers.routineId })
.from(routineTriggers)
.where(and(eq(routineTriggers.kind, "schedule"), eq(routineTriggers.enabled, true)));
const idsToPause = scheduledRoutineIds
.map((row) => row.routineId)
.filter((value): value is string => Boolean(value));
if (idsToPause.length === 0) {
return 0;
}
const paused = await db
.update(routines)
.set({
status: "paused",
updatedAt: new Date(),
})
.where(and(inArray(routines.id, idsToPause), sql`${routines.status} <> 'paused'`, sql`${routines.status} <> 'archived'`))
.returning({ id: routines.id });
return paused.length;
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
}
}
const EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY: SeededWorktreeExecutionQuarantineSummary = {
disabledTimerHeartbeats: 0,
resetRunningAgents: 0,
quarantinedInProgressIssues: 0,
unassignedTodoIssues: 0,
unassignedReviewIssues: 0,
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function isEnabledValue(value: unknown): boolean {
return value === true || value === "true" || value === 1 || value === "1";
}
function normalizeWorktreeRuntimeConfig(runtimeConfig: unknown): {
runtimeConfig: Record<string, unknown>;
disabledTimerHeartbeat: boolean;
changed: boolean;
} {
const nextRuntimeConfig = isRecord(runtimeConfig) ? { ...runtimeConfig } : {};
const heartbeat = isRecord(nextRuntimeConfig.heartbeat) ? { ...nextRuntimeConfig.heartbeat } : null;
if (!heartbeat) {
return { runtimeConfig: nextRuntimeConfig, disabledTimerHeartbeat: false, changed: false };
}
const disabledTimerHeartbeat = isEnabledValue(heartbeat.enabled);
if (heartbeat.enabled !== false) {
heartbeat.enabled = false;
nextRuntimeConfig.heartbeat = heartbeat;
return { runtimeConfig: nextRuntimeConfig, disabledTimerHeartbeat, changed: true };
}
return { runtimeConfig: nextRuntimeConfig, disabledTimerHeartbeat: false, changed: false };
}
export async function quarantineSeededWorktreeExecutionState(
connectionString: string,
): Promise<SeededWorktreeExecutionQuarantineSummary> {
const db = createDb(connectionString);
const summary = { ...EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY };
try {
await db.transaction(async (tx) => {
const seededAgents = await tx
.select({
id: agents.id,
status: agents.status,
runtimeConfig: agents.runtimeConfig,
})
.from(agents);
for (const agent of seededAgents) {
const normalized = normalizeWorktreeRuntimeConfig(agent.runtimeConfig);
const nextStatus = agent.status === "running" ? "idle" : agent.status;
if (normalized.disabledTimerHeartbeat) {
summary.disabledTimerHeartbeats += 1;
}
if (agent.status === "running") {
summary.resetRunningAgents += 1;
}
if (normalized.changed || nextStatus !== agent.status) {
await tx
.update(agents)
.set({
runtimeConfig: normalized.runtimeConfig,
status: nextStatus,
updatedAt: new Date(),
})
.where(eq(agents.id, agent.id));
}
}
const affectedIssues = await tx
.select({
id: issues.id,
companyId: issues.companyId,
status: issues.status,
})
.from(issues)
.where(
and(
sql`${issues.assigneeAgentId} is not null`,
sql`${issues.assigneeUserId} is null`,
inArray(issues.status, ["todo", "in_progress", "in_review"]),
),
);
for (const issue of affectedIssues) {
const nextStatus = issue.status === "in_progress" ? "blocked" : issue.status;
await tx
.update(issues)
.set({
status: nextStatus,
assigneeAgentId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
executionWorkspaceId: null,
updatedAt: new Date(),
})
.where(eq(issues.id, issue.id));
if (issue.status === "in_progress") {
summary.quarantinedInProgressIssues += 1;
await tx.insert(issueComments).values({
companyId: issue.companyId,
issueId: issue.id,
body:
"Quarantined during worktree seed so copied in-flight work does not auto-run in this isolated instance. " +
"Reassign or unblock here only if you intentionally want the worktree instance to own this task.",
});
} else if (issue.status === "todo") {
summary.unassignedTodoIssues += 1;
} else if (issue.status === "in_review") {
summary.unassignedReviewIssues += 1;
}
}
});
return summary;
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
}
}
async function seedWorktreeDatabase(input: {
sourceConfigPath: string;
sourceConfig: PaperclipConfig;
@@ -929,6 +1278,7 @@ async function seedWorktreeDatabase(input: {
targetPaths: WorktreeLocalPaths;
instanceId: string;
seedMode: WorktreeSeedMode;
preserveLiveWork?: boolean;
}): Promise<SeedWorktreeDatabaseResult> {
const seedPlan = resolveWorktreeSeedPlan(input.seedMode);
const sourceEnvFile = resolvePaperclipEnvFile(input.sourceConfigPath);
@@ -959,7 +1309,7 @@ async function seedWorktreeDatabase(input: {
const backup = await runDatabaseBackup({
connectionString: sourceConnectionString,
backupDir: path.resolve(input.targetPaths.backupDir, "seed"),
retentionDays: 7,
retention: { dailyDays: 7, weeklyWeeks: 4, monthlyMonths: 1 },
filenamePrefix: `${input.instanceId}-seed`,
includeMigrationJournal: true,
excludeTables: seedPlan.excludedTables,
@@ -979,6 +1329,10 @@ async function seedWorktreeDatabase(input: {
backupFile: backup.backupFile,
});
await applyPendingMigrations(targetConnectionString);
const executionQuarantine = input.preserveLiveWork
? { ...EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY }
: await quarantineSeededWorktreeExecutionState(targetConnectionString);
const pausedScheduledRoutines = await pauseSeededScheduledRoutines(targetConnectionString);
const reboundWorkspaces = await rebindSeededProjectWorkspaces({
targetConnectionString,
currentCwd: input.targetPaths.cwd,
@@ -986,6 +1340,8 @@ async function seedWorktreeDatabase(input: {
return {
backupSummary: formatDatabaseBackupResult(backup),
pausedScheduledRoutines,
executionQuarantine,
reboundWorkspaces,
};
} finally {
@@ -1064,6 +1420,8 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
const copiedGitHooks = copyGitHooksToWorktreeGitDir(cwd);
let seedSummary: string | null = null;
let seedExecutionQuarantineSummary: SeededWorktreeExecutionQuarantineSummary | null = null;
let pausedScheduledRoutineCount: number | null = null;
let reboundWorkspaceSummary: SeedWorktreeDatabaseResult["reboundWorkspaces"] = [];
if (opts.seed !== false) {
if (!sourceConfig) {
@@ -1081,8 +1439,11 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
targetPaths: paths,
instanceId,
seedMode,
preserveLiveWork: opts.preserveLiveWork,
});
seedSummary = seeded.backupSummary;
seedExecutionQuarantineSummary = seeded.executionQuarantine;
pausedScheduledRoutineCount = seeded.pausedScheduledRoutines;
reboundWorkspaceSummary = seeded.reboundWorkspaces;
spinner.stop(`Seeded isolated worktree database (${seedMode}).`);
} catch (error) {
@@ -1105,6 +1466,16 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
if (seedSummary) {
p.log.message(pc.dim(`Seed mode: ${seedMode}`));
p.log.message(pc.dim(`Seed snapshot: ${seedSummary}`));
if (opts.preserveLiveWork) {
p.log.warning("Preserved copied live work; this worktree instance may auto-run source-instance assignments.");
} else if (seedExecutionQuarantineSummary) {
p.log.message(
pc.dim(`Seed execution quarantine: ${formatSeededWorktreeExecutionQuarantineSummary(seedExecutionQuarantineSummary)}`),
);
}
if (pausedScheduledRoutineCount != null) {
p.log.message(pc.dim(`Paused scheduled routines: ${pausedScheduledRoutineCount}`));
}
for (const rebound of reboundWorkspaceSummary) {
p.log.message(
pc.dim(`Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`),
@@ -1172,18 +1543,7 @@ export async function worktreeMakeCommand(nameArg: string, opts: WorktreeMakeOpt
throw new Error(extractExecSyncErrorMessage(error) ?? String(error));
}
const installSpinner = p.spinner();
installSpinner.start("Installing dependencies...");
try {
execFileSync("pnpm", ["install"], {
cwd: targetPath,
stdio: ["ignore", "pipe", "pipe"],
});
installSpinner.stop("Installed dependencies.");
} catch (error) {
installSpinner.stop(pc.yellow("Failed to install dependencies (continuing anyway)."));
p.log.warning(extractExecSyncErrorMessage(error) ?? String(error));
}
installDependenciesBestEffort(targetPath);
const originalCwd = process.cwd();
try {
@@ -1200,6 +1560,21 @@ export async function worktreeMakeCommand(nameArg: string, opts: WorktreeMakeOpt
}
}
function installDependenciesBestEffort(targetPath: string): void {
const installSpinner = p.spinner();
installSpinner.start("Installing dependencies...");
try {
execFileSync("pnpm", ["install"], {
cwd: targetPath,
stdio: ["ignore", "pipe", "pipe"],
});
installSpinner.stop("Installed dependencies.");
} catch (error) {
installSpinner.stop(pc.yellow("Failed to install dependencies (continuing anyway)."));
p.log.warning(extractExecSyncErrorMessage(error) ?? String(error));
}
}
type WorktreeCleanupOptions = {
instance?: string;
home?: string;
@@ -1233,6 +1608,14 @@ type ResolvedWorktreeReseedSource = {
label: string;
};
type ResolvedWorktreeRepairTarget = {
rootPath: string;
configPath: string;
label: string;
branchName: string | null;
created: boolean;
};
function parseGitWorktreeList(cwd: string): GitWorktreeListEntry[] {
const raw = execFileSync("git", ["worktree", "list", "--porcelain"], {
cwd,
@@ -2674,10 +3057,7 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined,
}
}
export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promise<void> {
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclipai worktree reseed ")));
async function runWorktreeReseed(opts: WorktreeReseedOptions): Promise<void> {
const seedMode = opts.seedMode ?? "full";
if (!isWorktreeSeedMode(seedMode)) {
throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`);
@@ -2740,11 +3120,20 @@ export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promis
targetPaths,
instanceId: targetPaths.instanceId,
seedMode,
preserveLiveWork: opts.preserveLiveWork,
});
spinner.stop(`Reseeded ${targetEndpoint.label} (${seedMode}).`);
p.log.message(pc.dim(`Source: ${source.configPath}`));
p.log.message(pc.dim(`Target: ${targetEndpoint.configPath}`));
p.log.message(pc.dim(`Seed snapshot: ${seeded.backupSummary}`));
if (opts.preserveLiveWork) {
p.log.warning("Preserved copied live work; this worktree instance may auto-run source-instance assignments.");
} else {
p.log.message(
pc.dim(`Seed execution quarantine: ${formatSeededWorktreeExecutionQuarantineSummary(seeded.executionQuarantine)}`),
);
}
p.log.message(pc.dim(`Paused scheduled routines: ${seeded.pausedScheduledRoutines}`));
for (const rebound of seeded.reboundWorkspaces) {
p.log.message(
pc.dim(`Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`),
@@ -2757,6 +3146,98 @@ export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promis
}
}
export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promise<void> {
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclipai worktree reseed ")));
await runWorktreeReseed(opts);
}
export async function worktreeRepairCommand(opts: WorktreeRepairOptions): Promise<void> {
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclipai worktree repair ")));
const seedMode = opts.seedMode ?? "minimal";
if (!isWorktreeSeedMode(seedMode)) {
throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`);
}
const target = await ensureRepairTargetWorktree({
selector: nonEmpty(opts.branch) ?? undefined,
seedMode,
opts,
});
if (!target) {
p.log.warn("Current checkout is the primary repo worktree. Pass --branch to create or repair a linked worktree.");
p.outro(pc.yellow("No worktree repaired."));
return;
}
const source = resolveWorktreeRepairSource(opts);
if (!existsSync(source.configPath)) {
throw new Error(`Source config not found at ${source.configPath}.`);
}
if (path.resolve(source.configPath) === path.resolve(target.configPath)) {
throw new Error("Source and target Paperclip configs are the same. Use --from-config/--from-instance to point repair at a different source.");
}
const targetConfig = existsSync(target.configPath) ? readConfig(target.configPath) : null;
const targetEnvEntries = readPaperclipEnvEntries(resolvePaperclipEnvFile(target.configPath));
const targetHasWorktreeEnv = Boolean(
nonEmpty(targetEnvEntries.PAPERCLIP_HOME) && nonEmpty(targetEnvEntries.PAPERCLIP_INSTANCE_ID),
);
if (targetConfig && targetHasWorktreeEnv && opts.noSeed) {
p.log.message(pc.dim(`Target ${target.label} already has worktree-local config/env. Skipping reseed because --no-seed was passed.`));
p.outro(pc.green(`Worktree metadata already looks healthy for ${target.label}.`));
return;
}
if (targetConfig && targetHasWorktreeEnv) {
await runWorktreeReseed({
fromConfig: source.configPath,
to: target.rootPath,
seedMode,
preserveLiveWork: opts.preserveLiveWork,
yes: true,
allowLiveTarget: opts.allowLiveTarget,
});
return;
}
const repairInstanceId = sanitizeWorktreeInstanceId(path.basename(target.rootPath));
const repairPaths = resolveWorktreeLocalPaths({
cwd: target.rootPath,
homeDir: resolveWorktreeHome(opts.home),
instanceId: repairInstanceId,
});
const runningTargetPid = readRunningPostmasterPid(path.resolve(repairPaths.embeddedPostgresDataDir, "postmaster.pid"));
if (runningTargetPid && !opts.allowLiveTarget) {
throw new Error(
`Target worktree database appears to be running (pid ${runningTargetPid}). Stop Paperclip in ${target.rootPath} before repairing, or re-run with --allow-live-target if you want to override this guard.`,
);
}
if (runningTargetPid && opts.allowLiveTarget) {
p.log.warning(`Proceeding even though the target embedded PostgreSQL appears to be running (pid ${runningTargetPid}).`);
}
const originalCwd = process.cwd();
try {
process.chdir(target.rootPath);
await runWorktreeInit({
home: opts.home,
fromConfig: source.configPath,
fromDataDir: opts.fromDataDir,
fromInstance: opts.fromInstance,
seed: opts.noSeed ? false : true,
seedMode,
preserveLiveWork: opts.preserveLiveWork,
force: true,
});
} finally {
process.chdir(originalCwd);
}
}
export function registerWorktreeCommands(program: Command): void {
const worktree = program.command("worktree").description("Worktree-local Paperclip instance helpers");
@@ -2773,6 +3254,7 @@ export function registerWorktreeCommands(program: Command): void {
.option("--server-port <port>", "Preferred server port", (value) => Number(value))
.option("--db-port <port>", "Preferred embedded Postgres port", (value) => Number(value))
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
.option("--preserve-live-work", "Do not quarantine copied agent timers or assigned open issues in the seeded worktree", false)
.option("--no-seed", "Skip database seeding from the source instance")
.option("--force", "Replace existing repo-local config and isolated instance data", false)
.action(worktreeMakeCommand);
@@ -2789,6 +3271,7 @@ export function registerWorktreeCommands(program: Command): void {
.option("--server-port <port>", "Preferred server port", (value) => Number(value))
.option("--db-port <port>", "Preferred embedded Postgres port", (value) => Number(value))
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
.option("--preserve-live-work", "Do not quarantine copied agent timers or assigned open issues in the seeded worktree", false)
.option("--no-seed", "Skip database seeding from the source instance")
.option("--force", "Replace existing repo-local config and isolated instance data", false)
.action(worktreeInitCommand);
@@ -2828,10 +3311,25 @@ export function registerWorktreeCommands(program: Command): void {
.option("--from-data-dir <path>", "Source PAPERCLIP_HOME used when deriving the source config")
.option("--from-instance <id>", "Source instance id when deriving the source config")
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: full)", "full")
.option("--preserve-live-work", "Do not quarantine copied agent timers or assigned open issues in the seeded worktree", false)
.option("--yes", "Skip the destructive confirmation prompt", false)
.option("--allow-live-target", "Override the guard that requires the target worktree DB to be stopped first", false)
.action(worktreeReseedCommand);
worktree
.command("repair")
.description("Create or repair a linked worktree-local Paperclip instance without touching the primary checkout")
.option("--branch <name>", "Existing branch/worktree selector to repair, or a branch name to create under .paperclip/worktrees")
.option("--home <path>", `Home root for worktree instances (env: PAPERCLIP_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`)
.option("--from-config <path>", "Source config.json to seed from")
.option("--from-data-dir <path>", "Source PAPERCLIP_HOME used when deriving the source config")
.option("--from-instance <id>", "Source instance id when deriving the source config (default: default)")
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
.option("--preserve-live-work", "Do not quarantine copied agent timers or assigned open issues in the seeded worktree", false)
.option("--no-seed", "Repair metadata only and skip reseeding when bootstrapping a missing worktree config", false)
.option("--allow-live-target", "Override the guard that requires the target worktree DB to be stopped first", false)
.action(worktreeRepairCommand);
program
.command("worktree:cleanup")
.description("Safely remove a worktree, its branch, and its isolated instance data")

View File

@@ -0,0 +1,183 @@
import { execFileSync } from "node:child_process";
import {
ALL_INTERFACES_BIND_HOST,
LOOPBACK_BIND_HOST,
inferBindModeFromHost,
isAllInterfacesHost,
isLoopbackHost,
type BindMode,
type DeploymentExposure,
type DeploymentMode,
} from "@paperclipai/shared";
import type { AuthConfig, ServerConfig } from "./schema.js";
const TAILSCALE_DETECT_TIMEOUT_MS = 3000;
type BaseServerInput = {
port: number;
allowedHostnames: string[];
serveUi: boolean;
};
export function inferConfiguredBind(server?: Partial<ServerConfig>): BindMode {
if (server?.bind) return server.bind;
return inferBindModeFromHost(server?.customBindHost ?? server?.host);
}
export function detectTailnetBindHost(): string | undefined {
const explicit = process.env.PAPERCLIP_TAILNET_BIND_HOST?.trim();
if (explicit) return explicit;
try {
const stdout = execFileSync("tailscale", ["ip", "-4"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: TAILSCALE_DETECT_TIMEOUT_MS,
});
return stdout
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean);
} catch {
return undefined;
}
}
export function buildPresetServerConfig(
bind: Exclude<BindMode, "custom">,
input: BaseServerInput,
): { server: ServerConfig; auth: AuthConfig } {
const host =
bind === "loopback"
? LOOPBACK_BIND_HOST
: bind === "tailnet"
? (detectTailnetBindHost() ?? LOOPBACK_BIND_HOST)
: ALL_INTERFACES_BIND_HOST;
return {
server: {
deploymentMode: bind === "loopback" ? "local_trusted" : "authenticated",
exposure: "private",
bind,
customBindHost: undefined,
host,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
},
auth: {
baseUrlMode: "auto",
disableSignUp: false,
},
};
}
export function buildCustomServerConfig(input: BaseServerInput & {
deploymentMode: DeploymentMode;
exposure: DeploymentExposure;
host: string;
publicBaseUrl?: string;
}): { server: ServerConfig; auth: AuthConfig } {
const normalizedHost = input.host.trim();
const bind = isLoopbackHost(normalizedHost)
? "loopback"
: isAllInterfacesHost(normalizedHost)
? "lan"
: "custom";
return {
server: {
deploymentMode: input.deploymentMode,
exposure: input.deploymentMode === "local_trusted" ? "private" : input.exposure,
bind,
customBindHost: bind === "custom" ? normalizedHost : undefined,
host: normalizedHost,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
},
auth:
input.deploymentMode === "authenticated" && input.exposure === "public"
? {
baseUrlMode: "explicit",
disableSignUp: false,
publicBaseUrl: input.publicBaseUrl,
}
: {
baseUrlMode: "auto",
disableSignUp: false,
},
};
}
export function resolveQuickstartServerConfig(input: {
bind?: BindMode | null;
deploymentMode?: DeploymentMode | null;
exposure?: DeploymentExposure | null;
host?: string | null;
port: number;
allowedHostnames: string[];
serveUi: boolean;
publicBaseUrl?: string;
}): { server: ServerConfig; auth: AuthConfig } {
const trimmedHost = input.host?.trim();
const explicitBind = input.bind ?? null;
if (explicitBind === "loopback" || explicitBind === "lan" || explicitBind === "tailnet") {
return buildPresetServerConfig(explicitBind, {
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
});
}
if (explicitBind === "custom") {
return buildCustomServerConfig({
deploymentMode: input.deploymentMode ?? "authenticated",
exposure: input.exposure ?? "private",
host: trimmedHost || LOOPBACK_BIND_HOST,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
publicBaseUrl: input.publicBaseUrl,
});
}
if (trimmedHost) {
return buildCustomServerConfig({
deploymentMode: input.deploymentMode ?? (isLoopbackHost(trimmedHost) ? "local_trusted" : "authenticated"),
exposure: input.exposure ?? "private",
host: trimmedHost,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
publicBaseUrl: input.publicBaseUrl,
});
}
if (input.deploymentMode === "authenticated") {
if (input.exposure === "public") {
return buildCustomServerConfig({
deploymentMode: "authenticated",
exposure: "public",
host: ALL_INTERFACES_BIND_HOST,
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
publicBaseUrl: input.publicBaseUrl,
});
}
return buildPresetServerConfig("lan", {
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
});
}
return buildPresetServerConfig("loopback", {
port: input.port,
allowedHostnames: input.allowedHostnames,
serveUi: input.serveUi,
});
}

View File

@@ -50,7 +50,8 @@ program
.description("Interactive first-run setup wizard")
.option("-c, --config <path>", "Path to config file")
.option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP)
.option("-y, --yes", "Accept defaults (quickstart + start immediately)", false)
.option("--bind <mode>", "Quickstart reachability preset (loopback, lan, tailnet)")
.option("-y, --yes", "Accept quickstart defaults (trusted local loopback unless --bind is set) and start immediately", false)
.option("--run", "Start Paperclip immediately after saving config", false)
.action(onboard);
@@ -108,6 +109,7 @@ program
.option("-c, --config <path>", "Path to config file")
.option("-d, --data-dir <path>", DATA_DIR_OPTION_HELP)
.option("-i, --instance <id>", "Local instance id (default: default)")
.option("--bind <mode>", "On first run, use onboarding reachability preset (loopback, lan, tailnet)")
.option("--repair", "Attempt automatic repairs during doctor", true)
.option("--no-repair", "Disable automatic repairs during doctor")
.action(runCommand);

View File

@@ -1,6 +1,16 @@
import * as p from "@clack/prompts";
import { isLoopbackHost, type BindMode } from "@paperclipai/shared";
import type { AuthConfig, ServerConfig } from "../config/schema.js";
import { parseHostnameCsv } from "../config/hostnames.js";
import { buildCustomServerConfig, buildPresetServerConfig, inferConfiguredBind } from "../config/server-bind.js";
const TAILNET_BIND_WARNING =
"No Tailscale address was detected during setup. The saved config will stay on loopback until Tailscale is available or PAPERCLIP_TAILNET_BIND_HOST is set.";
function cancelled(): never {
p.cancel("Setup cancelled.");
process.exit(0);
}
export async function promptServer(opts?: {
currentServer?: Partial<ServerConfig>;
@@ -8,69 +18,37 @@ export async function promptServer(opts?: {
}): Promise<{ server: ServerConfig; auth: AuthConfig }> {
const currentServer = opts?.currentServer;
const currentAuth = opts?.currentAuth;
const currentBind = inferConfiguredBind(currentServer);
const deploymentModeSelection = await p.select({
message: "Deployment mode",
const bindSelection = await p.select({
message: "Reachability",
options: [
{
value: "local_trusted",
label: "Local trusted",
hint: "Easiest for local setup (no login, localhost-only)",
value: "loopback" as const,
label: "Trusted local",
hint: "Recommended for first run: localhost only, no login friction",
},
{
value: "authenticated",
label: "Authenticated",
hint: "Login required; use for private network or public hosting",
value: "lan" as const,
label: "Private network",
hint: "Broad private bind for LAN, VPN, or legacy --tailscale-auth style access",
},
{
value: "tailnet" as const,
label: "Tailnet",
hint: "Private authenticated access using the machine's detected Tailscale address",
},
{
value: "custom" as const,
label: "Custom",
hint: "Choose exact auth mode, exposure, and host manually",
},
],
initialValue: currentServer?.deploymentMode ?? "local_trusted",
initialValue: currentBind,
});
if (p.isCancel(deploymentModeSelection)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
const deploymentMode = deploymentModeSelection as ServerConfig["deploymentMode"];
let exposure: ServerConfig["exposure"] = "private";
if (deploymentMode === "authenticated") {
const exposureSelection = await p.select({
message: "Exposure profile",
options: [
{
value: "private",
label: "Private network",
hint: "Private access (for example Tailscale), lower setup friction",
},
{
value: "public",
label: "Public internet",
hint: "Internet-facing deployment with stricter requirements",
},
],
initialValue: currentServer?.exposure ?? "private",
});
if (p.isCancel(exposureSelection)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
exposure = exposureSelection as ServerConfig["exposure"];
}
const hostDefault = deploymentMode === "local_trusted" ? "127.0.0.1" : "0.0.0.0";
const hostStr = await p.text({
message: "Bind host",
defaultValue: currentServer?.host ?? hostDefault,
placeholder: hostDefault,
validate: (val) => {
if (!val.trim()) return "Host is required";
},
});
if (p.isCancel(hostStr)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
if (p.isCancel(bindSelection)) cancelled();
const bind = bindSelection as BindMode;
const portStr = await p.text({
message: "Server port",
@@ -84,15 +62,113 @@ export async function promptServer(opts?: {
},
});
if (p.isCancel(portStr)) {
p.cancel("Setup cancelled.");
process.exit(0);
if (p.isCancel(portStr)) cancelled();
const port = Number(portStr) || 3100;
const serveUi = currentServer?.serveUi ?? true;
if (bind === "loopback") {
return buildPresetServerConfig("loopback", {
port,
allowedHostnames: [],
serveUi,
});
}
if (bind === "lan" || bind === "tailnet") {
const allowedHostnamesInput = await p.text({
message: "Allowed private hostnames (comma-separated, optional)",
defaultValue: (currentServer?.allowedHostnames ?? []).join(", "),
placeholder:
bind === "tailnet"
? "your-machine.tailnet.ts.net"
: "dotta-macbook-pro, host.docker.internal",
validate: (val) => {
try {
parseHostnameCsv(val);
return;
} catch (err) {
return err instanceof Error ? err.message : "Invalid hostname list";
}
},
});
if (p.isCancel(allowedHostnamesInput)) cancelled();
const preset = buildPresetServerConfig(bind, {
port,
allowedHostnames: parseHostnameCsv(allowedHostnamesInput),
serveUi,
});
if (bind === "tailnet" && isLoopbackHost(preset.server.host)) {
p.log.warn(TAILNET_BIND_WARNING);
}
return preset;
}
const deploymentModeSelection = await p.select({
message: "Auth mode",
options: [
{
value: "local_trusted",
label: "Local trusted",
hint: "No login required; only safe with loopback-only or similarly trusted access",
},
{
value: "authenticated",
label: "Authenticated",
hint: "Login required; supports both private-network and public deployments",
},
],
initialValue: currentServer?.deploymentMode ?? "authenticated",
});
if (p.isCancel(deploymentModeSelection)) cancelled();
const deploymentMode = deploymentModeSelection as ServerConfig["deploymentMode"];
let exposure: ServerConfig["exposure"] = "private";
if (deploymentMode === "authenticated") {
const exposureSelection = await p.select({
message: "Exposure profile",
options: [
{
value: "private",
label: "Private network",
hint: "Private access only, with automatic URL handling",
},
{
value: "public",
label: "Public internet",
hint: "Internet-facing deployment with explicit public URL requirements",
},
],
initialValue: currentServer?.exposure ?? "private",
});
if (p.isCancel(exposureSelection)) cancelled();
exposure = exposureSelection as ServerConfig["exposure"];
}
const defaultHost =
currentServer?.customBindHost ??
currentServer?.host ??
(deploymentMode === "local_trusted" ? "127.0.0.1" : "0.0.0.0");
const host = await p.text({
message: "Bind host",
defaultValue: defaultHost,
placeholder: defaultHost,
validate: (val) => {
if (!val.trim()) return "Host is required";
if (deploymentMode === "local_trusted" && !isLoopbackHost(val.trim())) {
return "Local trusted mode requires a loopback host such as 127.0.0.1";
}
},
});
if (p.isCancel(host)) cancelled();
let allowedHostnames: string[] = [];
if (deploymentMode === "authenticated" && exposure === "private") {
const allowedHostnamesInput = await p.text({
message: "Allowed hostnames (comma-separated, optional)",
message: "Allowed private hostnames (comma-separated, optional)",
defaultValue: (currentServer?.allowedHostnames ?? []).join(", "),
placeholder: "dotta-macbook-pro, your-host.tailnet.ts.net",
validate: (val) => {
@@ -105,15 +181,11 @@ export async function promptServer(opts?: {
},
});
if (p.isCancel(allowedHostnamesInput)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
if (p.isCancel(allowedHostnamesInput)) cancelled();
allowedHostnames = parseHostnameCsv(allowedHostnamesInput);
}
const port = Number(portStr) || 3100;
let auth: AuthConfig = { baseUrlMode: "auto", disableSignUp: false };
let publicBaseUrl: string | undefined;
if (deploymentMode === "authenticated" && exposure === "public") {
const urlInput = await p.text({
message: "Public base URL",
@@ -133,32 +205,17 @@ export async function promptServer(opts?: {
}
},
});
if (p.isCancel(urlInput)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
auth = {
baseUrlMode: "explicit",
disableSignUp: false,
publicBaseUrl: urlInput.trim().replace(/\/+$/, ""),
};
} else if (currentAuth?.baseUrlMode === "explicit" && currentAuth.publicBaseUrl) {
auth = {
baseUrlMode: "explicit",
disableSignUp: false,
publicBaseUrl: currentAuth.publicBaseUrl,
};
if (p.isCancel(urlInput)) cancelled();
publicBaseUrl = urlInput.trim().replace(/\/+$/, "");
}
return {
server: {
deploymentMode,
exposure,
host: hostStr.trim(),
port,
allowedHostnames,
serveUi: currentServer?.serveUi ?? true,
},
auth,
};
return buildCustomServerConfig({
deploymentMode,
exposure,
host: host.trim(),
port,
allowedHostnames,
serveUi,
publicBaseUrl,
});
}

View File

@@ -32,10 +32,12 @@ Mode taxonomy and design intent are documented in `doc/DEPLOYMENT-MODES.md`.
Current CLI behavior:
- `paperclipai onboard` and `paperclipai configure --section server` set deployment mode in config
- server onboarding/configure ask for reachability intent and write `server.bind`
- `paperclipai run --bind <loopback|lan|tailnet>` passes a quickstart bind preset into first-run onboarding when config is missing
- runtime can override mode with `PAPERCLIP_DEPLOYMENT_MODE`
- `paperclipai run` and `paperclipai doctor` do not yet expose a direct `--mode` flag
- `paperclipai run` and `paperclipai doctor` still do not expose a direct low-level `--mode` flag
Target behavior (planned) is documented in `doc/DEPLOYMENT-MODES.md` section 5.
Canonical behavior is documented in `doc/DEPLOYMENT-MODES.md`.
Allow an authenticated/private hostname (for example custom Tailscale DNS):

View File

@@ -27,6 +27,18 @@ pnpm db:migrate
When `DATABASE_URL` is unset, this command targets the current embedded PostgreSQL instance for your active Paperclip config/instance.
Issue reference mentions follow the normal migration path: the schema migration creates the tracking table, but it does not backfill historical issue titles, descriptions, comments, or documents automatically.
To backfill existing content manually after migrating, run:
```sh
pnpm issue-references:backfill
# optional: limit to one company
pnpm issue-references:backfill -- --company <company-id>
```
Future issue, comment, and document writes sync references automatically without running the backfill command.
This mode is ideal for local development and one-command installs.
Docker note: the Docker quickstart image also uses embedded PostgreSQL by default. Persist `/paperclip` to keep DB state across container restarts (see `doc/DOCKER.md`).
@@ -94,6 +106,16 @@ Set `DATABASE_URL` in your `.env`:
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres
```
For hosted deployments that use a pooled runtime URL, set
`DATABASE_MIGRATION_URL` to the direct connection URL. Paperclip uses it for
startup schema checks/migrations and plugin namespace migrations, while the app
continues to use `DATABASE_URL` for runtime queries:
```sh
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres
DATABASE_MIGRATION_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
```
If using connection pooling (port 6543), the `postgres` client must disable prepared statements. Update `packages/db/src/client.ts`:
```ts

View File

@@ -17,6 +17,11 @@ Paperclip supports two runtime modes:
This keeps one authenticated auth stack while still separating low-friction private-network defaults from internet-facing hardening requirements.
Paperclip now treats **bind** as a separate concern from auth:
- auth model: `local_trusted` vs `authenticated`, plus `private/public`
- reachability model: `server.bind = loopback | lan | tailnet | custom`
## 2. Canonical Model
| Runtime Mode | Exposure | Human auth | Primary use |
@@ -25,6 +30,15 @@ This keeps one authenticated auth stack while still separating low-friction priv
| `authenticated` | `private` | Login required | Private-network access (for example Tailscale/VPN/LAN) |
| `authenticated` | `public` | Login required | Internet-facing/cloud deployment |
## Reachability Model
| Bind | Meaning | Typical use |
|---|---|---|
| `loopback` | Listen on localhost only | default local usage, reverse-proxy deployments |
| `lan` | Listen on all interfaces (`0.0.0.0`) | LAN/VPN/private-network access |
| `tailnet` | Listen on a detected Tailscale IP | Tailscale-only access |
| `custom` | Listen on an explicit host/IP | advanced interface-specific setups |
## 3. Security Policy
## `local_trusted`
@@ -38,12 +52,14 @@ This keeps one authenticated auth stack while still separating low-friction priv
- login required
- low-friction URL handling (`auto` base URL mode)
- private-host trust policy required
- bind can be `loopback`, `lan`, `tailnet`, or `custom`
## `authenticated + public`
- login required
- explicit public URL required
- stricter deployment checks and failures in doctor
- recommended bind is `loopback` behind a reverse proxy; direct `lan/custom` is advanced
## 4. Onboarding UX Contract
@@ -55,14 +71,22 @@ pnpm paperclipai onboard
Server prompt behavior:
1. ask mode, default `local_trusted`
2. option copy:
- `local_trusted`: "Easiest for local setup (no login, localhost-only)"
- `authenticated`: "Login required; use for private network or public hosting"
3. if `authenticated`, ask exposure:
- `private`: "Private network access (for example Tailscale), lower setup friction"
- `public`: "Internet-facing deployment, stricter security requirements"
4. ask explicit public URL only for `authenticated + public`
1. quickstart `--yes` defaults to `server.bind=loopback` and therefore `local_trusted/private`
2. advanced server setup asks reachability first:
- `Trusted local``bind=loopback`, `local_trusted/private`
- `Private network``bind=lan`, `authenticated/private`
- `Tailnet``bind=tailnet`, `authenticated/private`
- `Custom` → manual mode/exposure/host entry
3. raw host entry is only required for the `Custom` path
4. explicit public URL is only required for `authenticated + public`
Examples:
```sh
pnpm paperclipai onboard --yes
pnpm paperclipai onboard --yes --bind lan
pnpm paperclipai run --bind tailnet
```
`configure --section server` follows the same interactive behavior.
@@ -118,3 +142,4 @@ This prevents lockout when a user migrates from long-running local trusted usage
- implementation plan: `doc/plans/deployment-auth-mode-consolidation.md`
- V1 contract: `doc/SPEC-implementation.md`
- operator workflows: `doc/DEVELOPING.md` and `doc/CLI.md`
- invite/join state map: `doc/spec/invite-flow.md`

View File

@@ -43,6 +43,17 @@ This starts:
`pnpm dev` and `pnpm dev:once` are now idempotent for the current repo and instance: if the matching Paperclip dev runner is already alive, Paperclip reports the existing process instead of starting a duplicate.
## Storybook
The board UI Storybook keeps stories and Storybook config under `ui/storybook/` so component review files stay out of the app source routes.
```sh
pnpm storybook
pnpm build-storybook
```
These run the `@paperclipai/ui` Storybook on port `6006` and build the static output to `ui/storybook-static/`.
Inspect or stop the current repo's managed dev runner:
```sh
@@ -55,10 +66,23 @@ pnpm dev:stop
Tailscale/private-auth dev mode:
```sh
pnpm dev --tailscale-auth
pnpm dev --bind lan
```
This runs dev as `authenticated/private` and binds the server to `0.0.0.0` for private-network access.
This runs dev as `authenticated/private` with a private-network bind preset.
For Tailscale-only reachability on a detected tailnet address:
```sh
pnpm dev --bind tailnet
```
Legacy aliases still map to the old broad private-network behavior:
```sh
pnpm dev --tailscale-auth
pnpm dev --authenticated-private
```
Allow additional private hostnames (for example custom Tailscale hostnames):
@@ -66,6 +90,29 @@ Allow additional private hostnames (for example custom Tailscale hostnames):
pnpm paperclipai allowed-hostname dotta-macbook-pro
```
## Test Commands
Use the cheap local default unless you are specifically working on browser flows:
```sh
pnpm test
```
`pnpm test` runs the Vitest suite only. For interactive Vitest watch mode use:
```sh
pnpm test:watch
```
Browser suites stay separate:
```sh
pnpm test:e2e
pnpm test:release-smoke
```
These browser suites are intended for targeted local verification and CI, not the default agent/human test command.
## One-Command Local Run
For a first-time local install, you can bootstrap and run in one command:
@@ -173,9 +220,13 @@ Seed modes:
- `full` makes a full logical clone of the source instance
- `--no-seed` creates an empty isolated instance
Seeded worktree instances quarantine copied live execution by default for both `minimal` and `full` seeds. During restore, Paperclip disables copied agent timer heartbeats, resets copied `running` agents to `idle`, blocks and unassigns copied agent-owned `in_progress` issues, and unassigns copied agent-owned `todo`/`in_review` issues. This keeps a freshly booted worktree from starting agents for work already owned by the source instance. Pass `--preserve-live-work` only when you intentionally want the isolated worktree to resume copied assignments.
After `worktree init`, both the server and the CLI auto-load the repo-local `.paperclip/.env` when run inside that worktree, so normal commands like `pnpm dev`, `paperclipai doctor`, and `paperclipai db:backup` stay scoped to the worktree instance.
Provisioned git worktrees also pause all seeded routines in the isolated worktree database by default. This prevents copied daily/cron routines from firing unexpectedly inside the new workspace instance during development.
`pnpm dev` now fails fast in a linked git worktree when `.paperclip/.env` is missing, instead of silently booting against the default instance/port. If that happens, run `paperclipai worktree init` in the worktree first.
Provisioned git worktrees also pause seeded routines that still have enabled schedule triggers in the isolated worktree database by default. This prevents copied daily/cron routines from firing unexpectedly inside the new workspace instance during development without disabling webhook/API-only routines.
That repo-local env also sets:
@@ -184,6 +235,8 @@ That repo-local env also sets:
- `PAPERCLIP_WORKTREE_COLOR=<hex-color>`
The server/UI use those values for worktree-specific branding such as the top banner and dynamically colored favicon.
Authenticated worktree servers also use the `PAPERCLIP_INSTANCE_ID` value to scope Better Auth cookie names.
Browser cookies are shared by host rather than port, so this prevents logging into one `127.0.0.1:<port>` worktree from replacing another worktree server's session cookie.
Print shell exports explicitly when needed:
@@ -224,7 +277,7 @@ paperclipai worktree init --force
Repair an already-created repo-managed worktree and reseed its isolated instance from the main default install:
```sh
cd ~/.paperclip/worktrees/PAP-884-ai-commits-component
cd /path/to/paperclip/.paperclip/worktrees/PAP-884-ai-commits-component
pnpm paperclipai worktree init --force --seed-mode minimal \
--name PAP-884-ai-commits-component \
--from-config ~/.paperclip/instances/default/config.json
@@ -232,6 +285,33 @@ pnpm paperclipai worktree init --force --seed-mode minimal \
That rewrites the worktree-local `.paperclip/config.json` + `.paperclip/.env`, recreates the isolated instance under `~/.paperclip-worktrees/instances/<worktree-id>/`, and preserves the git worktree contents themselves.
For an already-created worktree where you want the CLI to decide whether to rebuild missing worktree metadata or just reseed the isolated DB, use `worktree repair`.
**`pnpm paperclipai worktree repair [options]`** — Repair the current linked worktree by default, or create/repair a named linked worktree under `.paperclip/worktrees/` when `--branch` is provided. The command never targets the primary checkout unless you explicitly pass `--branch`.
| Option | Description |
|---|---|
| `--branch <name>` | Existing branch/worktree selector to repair, or a branch name to create under `.paperclip/worktrees` |
| `--home <path>` | Home root for worktree instances (default: `~/.paperclip-worktrees`) |
| `--from-config <path>` | Source config.json to seed from |
| `--from-data-dir <path>` | Source `PAPERCLIP_HOME` used when deriving the source config |
| `--from-instance <id>` | Source instance id when deriving the source config (default: `default`) |
| `--seed-mode <mode>` | Seed profile: `minimal` or `full` (default: `minimal`) |
| `--no-seed` | Repair metadata only when bootstrapping a missing worktree config |
| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first |
Examples:
```sh
# From inside a linked worktree, rebuild missing .paperclip metadata and reseed it from the default instance.
cd /path/to/paperclip/.paperclip/worktrees/PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat
pnpm paperclipai worktree repair
# From the primary checkout, create or repair a linked worktree for a branch under .paperclip/worktrees/.
cd /path/to/paperclip
pnpm paperclipai worktree repair --branch PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat
```
For an already-created worktree where you want to keep the existing repo-local config/env and only overwrite the isolated database, use `worktree reseed` instead. Stop the target worktree's Paperclip server first so the command can replace the DB safely.
**`pnpm paperclipai worktree reseed [options]`** — Re-seed an existing worktree-local instance from another Paperclip instance or worktree while preserving the target worktree's current config, ports, and instance identity.

View File

@@ -3,7 +3,7 @@ Use this exact checklist.
1. Start Paperclip in auth mode.
```bash
cd <paperclip-repo-root>
pnpm dev --tailscale-auth
pnpm dev --bind lan
```
Then verify:
```bash

View File

@@ -115,38 +115,6 @@ If the first real publish returns npm `E404`, check npm-side prerequisites befor
- The initial publish must include `--access public` for a public scoped package.
- npm also requires either account 2FA for publishing or a granular token that is allowed to bypass 2FA.
### Manual first publish for `@paperclipai/mcp-server`
If you need to publish only the MCP server package once by hand, use:
- `@paperclipai/mcp-server`
Recommended flow from the repo root:
```bash
# optional sanity check: this 404s until the first publish exists
npm view @paperclipai/mcp-server version
# make sure the build output is fresh
pnpm --filter @paperclipai/mcp-server build
# confirm your local npm auth before the real publish
npm whoami
# safe preview of the exact publish payload
cd packages/mcp-server
pnpm publish --dry-run --no-git-checks --access public
# real publish
pnpm publish --no-git-checks --access public
```
Notes:
- Publish from `packages/mcp-server/`, not the repo root.
- If `npm view @paperclipai/mcp-server version` already returns the same version that is in [`packages/mcp-server/package.json`](../packages/mcp-server/package.json), do not republish. Bump the version or use the normal repo-wide release flow in [`scripts/release.sh`](../scripts/release.sh).
- The same npm-side prerequisites apply as above: valid npm auth, permission to publish to the `@paperclipai` scope, `--access public`, and the required publish auth/2FA policy.
## Version formats
Paperclip uses calendar versions:

View File

@@ -395,6 +395,8 @@ Side effects:
- entering `done` sets `completed_at`
- entering `cancelled` sets `cancelled_at`
Detailed ownership, execution, blocker, and crash-recovery semantics are documented in `doc/execution-semantics.md`.
## 8.3 Approval Status
- `pending -> approved | rejected | cancelled`
@@ -617,7 +619,7 @@ Per-agent schedule fields in `adapter_config`:
- `enabled` boolean
- `intervalSec` integer (minimum 30)
- `maxConcurrentRuns` fixed at `1` for V1
- `maxConcurrentRuns` integer; new agents default to `5`
Scheduler must skip invocation when:

122
doc/design-system/REVIEW.md Normal file
View File

@@ -0,0 +1,122 @@
# Paperclip DS Extraction — Review
- **Generated:** 2026-04-21
- **Repo SHA:** `a26e1288b627e82c554445732c7d844648e6b5e1`
- **Branch:** `sockmonster-ds-extraction`
- **Discovery config:** [`_discovery.json`](./_discovery.json)
- **Scope:** `ui/` (`@paperclipai/ui`). Plugin SDK (`packages/plugins/sdk/src/ui/`) treated as contract surface, not implementation surface.
This is the entry point. Everything else is linked from here. Contents are ordered by **expected human value**, not by stage.
---
## Bottom line
One finding sits upstream of most of the others — resolving it moves four pattern docs from "pending" to "codifiable" and unblocks the single biggest token gap.
> **The app has a canonical status/priority color catalog (`ui/src/lib/status-colors.ts`) that bypasses the DS token layer and uses raw Tailwind palette classes across 11 hues and ~24 status keys.** Status indicators (`StatusIcon`, `StatusBadge`, `PriorityIcon`, `agentStatusDot`), chart colors (`ActivityCharts.tsx`, hardcoded hex), budget severity indicators (`BudgetPolicyCard`, `BudgetIncidentCard`, `BudgetSidebarMarker`), and quota fills (`QuotaBar`) are **four distinct systems encoding the same red/amber/green severity concept**, none of which share DS tokens.
A `--signal-*` token family would collapse four surfaces onto one vocabulary and make [status-display.md](./patterns/status-display.md), [quota-display.md](./patterns/quota-display.md), and the severity-indicator pattern opportunity all codifiable. See [tokens-review.md §4](./tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds) and [patterns-review.md §6](./patterns/patterns-review.md#6-severity-indicator-3-level-health-display--pattern-opportunity).
Three other findings are high-value but smaller in scope:
- **`destructive-foreground` has a buggy light-mode value** equal to `destructive` itself (would render invisible if anyone used it — nobody does, so the bug is masked). [tokens-review.md §2](./tokens/tokens-review.md#2-destructive-foreground-has-a-wrong-light-mode-value-and-is-unused)
- **13 color tokens are dead** (all 5 `chart-*`, all 8 `sidebar-*`). Consolidating would drop color-token count from 32 → 19. [tokens-review.md §1, §3](./tokens/tokens-review.md#1-chart--tokens-are-dead)
- **The radius scale is non-monotonic and under-specified** — 227 uses of `rounded-lg` / `rounded-xl` resolve to square corners because `--radius-lg` / `--radius-xl` = 0. Needs a founder call on whether this is intentional flat-design or a stale migration state. [tokens-review.md §Radius scale](./tokens/tokens-review.md#radius-scale--under-founder-review)
---
## Recommended review order
Sequenced so each step unblocks the next. Total time estimated ~23 hours.
| # | Read | Decide | Est. |
|---|---|---|---|
| 1 | [tokens-review.md §High-confidence drift](./tokens/tokens-review.md#high-confidence-drift-likely-should-be-fixed) | Scope the signal-token work. Confirm dead-token deletions (chart-*, sidebar-*, destructive-foreground). | **25 min** |
| 2 | [tokens-review.md §Radius scale](./tokens/tokens-review.md#radius-scale--under-founder-review) | One call: intentional flat lg/xl, or restore a monotonic scale. | **15 min** |
| 3 | [components-review.md §Likely duplicates](./components/components-review.md#likely-duplicates) | Nine duplicate families. For each, note "merge/keep/defer" — patterns flow from the decisions. | **30 min** |
| 4 | [components-review.md §Plugin SDK contract gap](./components/components-review.md#plugin-sdk-contract-gap) | Choose: fulfill the 9 missing contracts, shrink them, or hybrid. | **15 min** |
| 5 | [patterns-review.md §Variance across documented patterns](./patterns/patterns-review.md#variance-across-documented-patterns-whats-inconsistent-between-instances) | Look at the status-element variance in detail pages (four different treatments across eight pages). | **15 min** |
| 6 | [patterns-review.md §Paperclip-domain patterns](./patterns/patterns-review.md#paperclip-domain-patterns-worth-calling-out-opportunities-not-ratified-patterns) | Reality-check the run-transcript / heartbeat / metric-cell opportunities before any codify step. | **20 min** |
| 7 | [components-review.md §Naming inconsistencies](./components/components-review.md#naming-inconsistencies) | Lower priority — no decision required today, but at least skim. | **10 min** |
| 8 | [components-review.md §Story coverage gaps](./components/components-review.md#story-coverage-gaps) | Shadcn primitives missing from `foundations.stories.tsx` (`collapsible`, `dropdown-menu`, `avatar`, `skeleton`, `scroll-area`) is a small, targeted fix. | **10 min** |
---
## Confidence
### High confidence (probably correct, spot-check only)
- **32 color tokens** extracted from `ui/src/index.css` (19 semantic surfaces, 5 chart, 8 sidebar).
- **5 radius tokens**, with value + definition-site recorded.
- **Usage counts per color and radius token** computed by unioning Tailwind-utility occurrences and `var(--token)` references across `ui/src/**/*.{ts,tsx,css}` (excluding the definition file itself). Counts are rough by intent — within ±10%.
- **135 component files** enumerated, classified into 22 primitives / 64 composites / 47 standalones / 2 non-component utilities.
- **104 components** cross-referenced against 14 Storybook files via import-graph parsing.
- **50 pages** enumerated; per-page import set captured.
- **11 plugin SDK ambient components** enumerated with host-implementation status.
- **4 components confirmed as storybook-only** (0 production uses): `AccountingModelCard`, `AgentProperties`, `CompanySwitcher`, `ExecutionParticipantPicker`.
### Medium confidence (review carefully)
- **Duplicate-family flags.** Eight families surfaced ([components-review.md §Likely duplicates](./components/components-review.md#likely-duplicates)) are based on name parallelism and/or shared imports. The strongest signals (entity-creation dialogs, subscription panels) need a side-by-side diff to confirm merge-ability; this extraction didn't do that.
- **`BillerSpendCard` vs `FinanceBillerCard` as likely-true-duplicate.** Flagged per directive. Not confirmed without a diff.
- **Pattern instance counts.** The `detail-page` and `list-page` patterns were identified by import-set intersection, which is a proxy for structural similarity. A page can import a component and not actually render it in the expected position; pattern shape is inferred, not verified pixel-by-pixel.
- **CVA variant extraction for primitives.** Parsed 3 files successfully (`button`, `badge`, one more). The rest of the primitives likely have variants that the static parser missed.
- **The severity-indicator pattern ([patterns-review.md §6](./patterns/patterns-review.md#6-severity-indicator-3-level-health-display--pattern-opportunity)).** Named as an *opportunity*, not a ratified pattern — cross-system evidence is strong but the four systems weren't compared pixel-for-pixel; they may not actually agree on what "warning" looks like.
- **Story coverage set.** Computed by parsing imports in `.stories.tsx` files. A component that's imported by a story but never actually rendered would falsely appear covered. Low risk given story-file structure but not validated.
### Low confidence (likely wrong, incomplete, or judgment-heavy)
- **Motion tokens.** None exist as variables — motion is inline `@keyframes` + `cubic-bezier()`. Pattern docs don't describe motion. The 5 keyframes in `index.css` are listed; their callers are not cross-referenced.
- **Typography.** No project-local font/type tokens found — the section is near-empty because Tailwind v4 defaults carry the load plus `@tailwindcss/typography`. If there are intended type-scale conventions in components that weren't captured by token extraction, those are missed.
- **Elevation / shadows.** No tokens, so no inventory. Ad-hoc `shadow-[…]` values across polished surfaces were enumerated in [tokens-review.md §9](./tokens/tokens-review.md#9-arbitrary-shadow-values-in-production-surfaces), but the list is not exhaustive.
- **Prop extraction for primitives using `React.ComponentProps<"button"> & VariantProps<...>`.** The static parser looks for `*Props` interfaces; inline-type components (most shadcn primitives) get "no Props interface found" in their detail files.
- **Per-component token consumption cross-reference.** Components/detail files don't list which specific tokens each component consumes (would require per-file class-attribute parsing). Token usage counts are global; per-component token drift is flagged only where specific drift was found.
- **Pattern: "detail-page header."** Called out as a sub-pattern inside detail-page doc but not given its own file — instances share only 45 imports, not a complete shape.
---
## Known scope limitations
- **Plugin SDK UI.** In-scope as a contract surface (documented in [components/index.md §Plugin SDK contracts](./components/index.md#plugin-sdk-contracts-11)). Not in-scope for pattern extraction — host implementations are covered; plugin-side usage patterns are not.
- **Low-usage components (12 code imports, 76 of them).** Listed in [components/index.md](./components/index.md) with status marker `📘 below-threshold`; no dedicated detail file. Per the directive: *nothing gets silently dropped*.
- **Pattern documentation:** capped at 10 real patterns. Eleven pattern files exist because the duplicate-family directive required documenting three below-threshold pairs (subscription-panel, sidebar-menu pair inside sidebar-chrome, quota-display). Pattern opportunities surfaced in patterns-review.md are not yet pattern files.
- **UX Lab pages (`InviteUxLab`, `IssueChatUxLab`, `RunTranscriptUxLab`).** Acknowledged prototypes with distinct visual language. Excluded from pattern extraction. Their raw-palette usage is counted in drift stats but not pursued.
- **Hermes / adapter code.** `ui/src/adapters/` contains per-adapter config fields. Not a DS concern; skipped.
- **Mobile treatments.** `MobileBottomNav` and `SwipeToArchive` are noted but not extracted as their own pattern. Mobile patterns appear to live inside individual list/detail pages rather than as shared primitives.
- **Diff mode.** This is a fresh run; `doc/design-system/` did not exist before. No diff was generated. Subsequent re-runs should run in diff mode (see [ds-extraction skill §Diff mode](../../.agents/skills/ds-extraction/SKILL.md#diff-mode)).
---
## What's on disk
```
doc/design-system/
├── REVIEW.md ← you are here
├── _discovery.json ← Stage 0 output
├── _pages.json ← Stage 2 scratch (50 pages)
├── _composition-graph.json ← Stage 2 scratch (135 components)
├── _stories.json ← Stage 2 scratch (14 stories)
├── tokens/
│ ├── tokens.md ← canonical human-readable inventory
│ ├── tokens.json ← machine-readable for downstream tooling
│ └── tokens-review.md ← the high-value drift artifact
├── components/
│ ├── index.md ← all 135 files + 11 SDK contracts, with status markers
│ ├── components-review.md ← duplicates, naming, token non-compliance, story gaps, SDK gap
│ └── [ComponentName].md × 53 ← per-component detail files (3+ uses threshold)
└── patterns/
├── index.md
├── patterns-review.md ← variance, opportunities, what to resolve before re-running
├── list-page.md ← 12 instances
├── detail-page.md ← 8 instances
├── sidebar-chrome.md ← 6 + 2 instances
├── finance-card.md ← 5 instances
├── entity-properties-panel.md ← 4 + 1 instances (open Q on generic)
├── entity-creation-dialog.md ← 4 instances
├── status-display.md ← 3 components + catalog (pending signal tokens)
├── entity-row.md ← 3 instances
├── subscription-panel.md ← 2 instances (below threshold — documented)
└── quota-display.md ← 2 instances (below threshold — documented)
```
Total: **~80 files**.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,229 @@
{
"generated_at": "2026-04-21T00:00:00Z",
"repo_sha": "a26e1288b627e82c554445732c7d844648e6b5e1",
"branch": "sockmonster-ds-extraction",
"styling": {
"tailwind_version": "v4",
"tailwind_config": null,
"tailwind_config_note": "No tailwind.config.* file. Tailwind v4 CSS-first config: theme is declared via @theme inline blocks in ui/src/index.css. Build integration via @tailwindcss/vite.",
"css_variables_file": "ui/src/index.css",
"uses_css_variables": true,
"uses_cva": true,
"uses_cn_helper": true,
"cn_helper_location": "ui/src/lib/utils.ts:6",
"shadcn_present": true,
"shadcn_style": "new-york",
"shadcn_base_color": "neutral",
"shadcn_css_variables": true,
"shadcn_rsc": false,
"shadcn_icon_library": "lucide",
"shadcn_aliases": {
"components": "@/components",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks",
"utils": "@/lib/utils"
},
"shadcn_skill_path": ".agents/skills/shadcn/SKILL.md",
"other_styling": [
{
"library": "@tailwindcss/typography",
"usage": "@plugin in ui/src/index.css; prose class styling for markdown"
}
],
"notes": "Tailwind v4 with shadcn/ui in new-york style. components.json present. cn() = clsx + tailwind-merge. Custom @custom-variant dark (&:is(.dark *)). No tailwindcss-animate plugin."
},
"tokens": {
"color": {
"sources": ["ui/src/index.css"],
"authoritative_source": "ui/src/index.css",
"definition_blocks": [
{ "block": "@theme inline", "role": "exposes --color-* aliases to Tailwind", "line_range": "6-43" },
{ "block": ":root", "role": "authoritative light-mode values", "line_range": "45-80" },
{ "block": ".dark", "role": "dark-mode overrides", "line_range": "82-115" }
],
"count_estimate": 32,
"categories": {
"semantic_neutral_and_intent": [
"background", "foreground", "card", "card-foreground", "popover", "popover-foreground",
"primary", "primary-foreground", "secondary", "secondary-foreground",
"muted", "muted-foreground", "accent", "accent-foreground",
"destructive", "destructive-foreground",
"border", "input", "ring"
],
"chart": ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"],
"sidebar": [
"sidebar", "sidebar-foreground",
"sidebar-primary", "sidebar-primary-foreground",
"sidebar-accent", "sidebar-accent-foreground",
"sidebar-border", "sidebar-ring"
]
},
"includes_signal_green": false,
"value_format": "oklch",
"dark_mode_convention": ".dark class selector via @custom-variant"
},
"spacing": {
"sources": [],
"authoritative_source": null,
"count_estimate": 0,
"note": "No project-local spacing tokens. Spacing uses Tailwind v4 defaults inherited from tailwindcss package."
},
"type": {
"sources": [],
"authoritative_source": null,
"count_estimate": 0,
"font_faces": [],
"google_fonts": [],
"note": "No project-local font/type tokens. Typography uses Tailwind v4 defaults + @tailwindcss/typography plugin. Markdown styling via `.paperclip-markdown` and `.paperclip-mdxeditor-content` classes with hardcoded font-size/line-height values in index.css."
},
"radius": {
"sources": ["ui/src/index.css"],
"authoritative_source": "ui/src/index.css",
"count_estimate": 5,
"tokens": [
{ "name": "--radius", "value": "0", "defined_at": "ui/src/index.css:47", "scope": ":root" },
{ "name": "--radius-sm", "value": "0.375rem", "defined_at": "ui/src/index.css:39", "scope": "@theme" },
{ "name": "--radius-md", "value": "0.5rem", "defined_at": "ui/src/index.css:40", "scope": "@theme" },
{ "name": "--radius-lg", "value": "0px", "defined_at": "ui/src/index.css:41", "scope": "@theme" },
{ "name": "--radius-xl", "value": "0px", "defined_at": "ui/src/index.css:42", "scope": "@theme" }
],
"note": "Unusual: --radius-lg and --radius-xl are 0px while --radius-sm and --radius-md are non-zero. Likely intentional flat-design choice at the outer scale, but worth confirming. Also --radius (base, :root) = 0 used by MDXEditor integration; not part of @theme."
},
"motion": {
"sources": ["ui/src/index.css"],
"authoritative_source": null,
"count_estimate": 0,
"css_variable_tokens": [],
"keyframes": [
{ "name": "dashboard-activity-enter", "defined_at": "ui/src/index.css:228" },
{ "name": "dashboard-activity-highlight", "defined_at": "ui/src/index.css:246" },
{ "name": "cot-line-slide-in", "defined_at": "ui/src/index.css:272" },
{ "name": "cot-line-slide-out", "defined_at": "ui/src/index.css:277" },
{ "name": "shimmer-text-slide", "defined_at": "ui/src/index.css:298" }
],
"tailwindcss_animate_plugin": false,
"note": "No --motion-* or --duration-* tokens. Motion is defined as inline @keyframes + inline cubic-bezier values (commonly cubic-bezier(0.16, 1, 0.3, 1) and cubic-bezier(0.4, 0, 0.2, 1)). prefers-reduced-motion respected."
},
"elevation": {
"sources": [],
"authoritative_source": null,
"count_estimate": 0,
"note": "No --shadow-* tokens and no theme.boxShadow. Project appears to avoid shadows as a design choice (borders and background shifts carry elevation). Verify during extraction by grepping for box-shadow usage in components."
},
"scoped_non_ds_variables": [
{
"group": "MDXEditor theme bridge",
"selector": ".paperclip-mdxeditor-scope, .paperclip-mdxeditor",
"line_range": "332-361",
"variable_count": 24,
"role": "Maps host DS tokens onto MDXEditor's internal token names (--baseBase, --accentSolid, etc.). Consumed alias layer, not authoritative DS tokens."
},
{
"group": "Shimmer text effect",
"selector": ".shimmer-text",
"variable_count": 2,
"role": "Component-local (--shimmer-base, --shimmer-highlight)."
}
]
},
"components": {
"primary_root": "ui/src/components/",
"layout": "mixed",
"layout_notes": "Not purely flat and not purely nested. Subdirectories exist for a specific subset; the majority live flat at the top level. Naming convention for primitives is lowercase-kebab (button.tsx, dropdown-menu.tsx); composites/features use PascalCase (AgentConfigForm.tsx).",
"subdirectories": [
{ "path": "ui/src/components/ui/", "role": "shadcn primitives", "file_count": 22 },
{ "path": "ui/src/components/access/", "role": "access-control feature cluster", "file_count": 3 },
{ "path": "ui/src/components/transcript/", "role": "run transcript feature cluster", "file_count": 2 }
],
"top_level_tsx_count": 108,
"primitives_in_components_ui": [
"avatar", "badge", "breadcrumb", "button", "card", "checkbox", "collapsible",
"command", "dialog", "dropdown-menu", "input", "label", "popover",
"scroll-area", "select", "separator", "sheet", "skeleton", "tabs",
"textarea", "toggle-switch", "tooltip"
],
"count_estimate": 133,
"plugin_sdk_components": {
"path": "packages/plugins/sdk/src/ui/components.ts",
"status": "ambient-types-only",
"count": 11,
"declared_components": [
"MetricCard", "StatusBadge", "DataTable", "TimeseriesChart", "MarkdownBlock",
"KeyValueList", "ActionBar", "LogView", "JsonTree", "Spinner", "ErrorBoundary"
],
"runtime_model": "Host provides implementations via renderSdkUiComponent(name, props) runtime injection. Plugin bundles ship type declarations only.",
"name_collision_check": "MetricCard.tsx and StatusBadge.tsx exist at ui/src/components/ top-level. The other 9 declared components have no obvious matching file — may exist under different names (e.g., MarkdownBody ≈ MarkdownBlock, JsonSchemaForm unrelated) or may not be implemented yet."
}
},
"usage_surfaces": {
"pages_root": "ui/src/pages/",
"page_count_estimate": 50,
"page_count_method": "find ui/src/pages -name '*.tsx' -not -name '*.test.tsx'",
"other_surfaces": [
{ "path": "ui/src/App.tsx", "role": "root router" },
{ "path": "ui/src/plugins/", "role": "plugin slot & launcher rendering (slots.tsx, launchers.tsx)" }
],
"notable_pages": {
"design_guide": "ui/src/pages/DesignGuide.tsx — an existing in-app design reference page. Imports shadcn primitives; worth cross-referencing during Stage 3 for intended-vs-actual primitive usage. Its presence means a partial DS narrative already exists in-code.",
"ux_labs": [
"ui/src/pages/InviteUxLab.tsx",
"ui/src/pages/IssueChatUxLab.tsx",
"ui/src/pages/RunTranscriptUxLab.tsx"
]
}
},
"storybook": {
"present": true,
"version": "10.3.5",
"config_path": "ui/storybook/.storybook/main.ts",
"stories_location": "centralized",
"stories_glob": "ui/storybook/stories/**/*.stories.@(ts|tsx|mdx)",
"story_file_count": 14,
"story_organization": "thematic",
"story_organization_note": "Stories are organized by domain/theme (foundations, navigation-layout, dialogs-modals, chat-comments, forms-editors, status-language, data-viz-misc, agent-management, issue-management, projects-goals-workspaces, budget-finance, control-plane-surfaces, ux-labs, overview) — NOT one-story-per-component. A single .stories.tsx file typically imports and composes many components. 'Component covered by story' must be computed by parsing import graphs of story files, not by file naming.",
"story_files": [
"foundations.stories.tsx",
"overview.stories.tsx",
"status-language.stories.tsx",
"navigation-layout.stories.tsx",
"dialogs-modals.stories.tsx",
"forms-editors.stories.tsx",
"chat-comments.stories.tsx",
"data-viz-misc.stories.tsx",
"agent-management.stories.tsx",
"issue-management.stories.tsx",
"projects-goals-workspaces.stories.tsx",
"budget-finance.stories.tsx",
"control-plane-surfaces.stories.tsx",
"ux-labs.stories.tsx"
],
"addons": ["@storybook/addon-docs", "@storybook/addon-a11y"],
"covered_components": null,
"covered_components_note": "Deferred to Stage 2 (parse story imports). Discovery only confirms stories exist, not per-component coverage."
},
"existing_docs": {
"design_system_dir_present": false,
"locations": [
{ "path": "ui/README.md", "role": "package readme; non-DS" },
{ "path": "ui/src/pages/DesignGuide.tsx", "role": "in-app design reference page (component-level showcase)" }
],
"figma_sync_config": null,
"style_dictionary_config": null,
"tokens_studio_config": null
},
"known_gaps": [
"Plugin SDK UI is a types-only ambient bridge (packages/plugins/sdk/src/ui/components.ts). The host-provided component kit promised by the SDK is partial: only MetricCard and StatusBadge have matching host implementations by name. 9 other declared components (DataTable, TimeseriesChart, MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree, Spinner, ErrorBoundary) have no obvious host implementation. PLUGIN_SPEC.md:30 confirms: 'The current runtime does not yet ship a real host-provided plugin UI component kit'.",
"No dedicated spacing, type, or elevation tokens. Those categories rely on Tailwind v4 defaults. Extraction should not synthesize repo-specific tokens where none exist.",
"No central motion token language. Motion is expressed as per-feature @keyframes with inline easing. Treat motion as a candidate for future tokenization rather than documenting a current system.",
"No Figma/style-dictionary/tokens-studio integration. The design system is code-authored, not design-tool-synced."
],
"uncertainties": [
"Storybook organization is thematic (14 composite stories), not per-component. The extraction skill's 'covered_by_story' signal needs to be computed by parsing each story file's import graph and surfacing components used inside render bodies. Flag before Stage 2 so the skill doesn't default to file-name matching.",
"Radius scale is non-monotonic: --radius-sm = 0.375rem, --radius-md = 0.5rem, --radius-lg = 0px, --radius-xl = 0px. Flat-design choice or stale values? Also --radius (base) = 0 at :root coexists with the @theme tokens; which is canonical for Tailwind rounded utilities? Worth confirming before Stage 1 drift analysis flags every rounded-lg usage.",
"Plugin SDK UI contracts 11 shared components but only 2 appear to be implemented by that name in ui/. For extraction scope, should we (a) treat the SDK declarations as DS contract and flag missing implementations as gaps, or (b) ignore the SDK and document only what exists in ui/? Recommendation: (a) — it's higher-value signal for the human reviewer.",
"ui/src/components/ has a mixed layout: 22 shadcn primitives in a 'ui/' subdirectory and 108 components flat at the top level. The top-level mix contains features, composites, one-off pages pieces, and true reusable patterns. Stage 3 will need a heuristic (composition-graph-based) rather than directory-based category inference.",
"Total component count (~133) is meaningfully larger than the skill's illustrative example (87). With the 3+-usage threshold for detail files, output should stay tractable, but the skill should confirm the threshold is right at this scale before Stage 3 starts generating per-component .md files.",
"MDXEditor CSS variable bridge (.paperclip-mdxeditor-scope, 24 --base*/--accent* variables) is DS-adjacent — it consumes host tokens and maps them to MDXEditor internals. Should Stage 1 include these in tokens.json? Recommendation: no — they are consumed aliases, not authoritative tokens. Flag them in tokens-review.md as 'integration layer' rather than as drift."
]
}

View File

@@ -0,0 +1,646 @@
{
"generated_at": "2026-04-21T00:00:00Z",
"repo_sha": "a26e1288b627e82c554445732c7d844648e6b5e1",
"page_count": 50,
"method": "Import-graph extraction: 'from \"@/components/<name>\"' and relative imports. Top-level JSX tree not parsed \u2014 import set is the proxy for rendered-components set. Components a page imports but never renders would inflate the count slightly; low risk here given app style.",
"pages": {
"Activity": {
"path": "ui/src/pages/Activity.tsx",
"components_imported": [
"ActivityRow",
"EmptyState",
"PageSkeleton",
"select"
],
"component_count": 4
},
"AdapterManager": {
"path": "ui/src/pages/AdapterManager.tsx",
"components_imported": [
"PathInstructionsModal",
"badge",
"button",
"card",
"dialog",
"input",
"label"
],
"component_count": 7
},
"AgentDetail": {
"path": "ui/src/pages/AgentDetail.tsx",
"components_imported": [
"ActivityCharts",
"AgentActionButtons",
"AgentConfigForm",
"AgentIconPicker",
"BudgetPolicyCard",
"CopyText",
"EntityRow",
"Identity",
"MarkdownBody",
"MarkdownEditor",
"PackageFileTree",
"PageSkeleton",
"PageTabBar",
"RunTranscriptView",
"ScrollToBottom",
"StatusBadge",
"agent-config-primitives",
"button",
"collapsible",
"input",
"popover",
"skeleton",
"tabs",
"toggle-switch",
"tooltip"
],
"component_count": 25
},
"Agents": {
"path": "ui/src/pages/Agents.tsx",
"components_imported": [
"EmptyState",
"EntityRow",
"PageSkeleton",
"PageTabBar",
"StatusBadge",
"button",
"tabs"
],
"component_count": 7
},
"ApprovalDetail": {
"path": "ui/src/pages/ApprovalDetail.tsx",
"components_imported": [
"ApprovalPayload",
"Identity",
"MarkdownBody",
"PageSkeleton",
"StatusBadge",
"button",
"textarea"
],
"component_count": 7
},
"Approvals": {
"path": "ui/src/pages/Approvals.tsx",
"components_imported": [
"ApprovalCard",
"PageSkeleton",
"PageTabBar",
"tabs"
],
"component_count": 4
},
"Auth": {
"path": "ui/src/pages/Auth.tsx",
"components_imported": [
"AsciiArtAnimation",
"button"
],
"component_count": 2
},
"BoardClaim": {
"path": "ui/src/pages/BoardClaim.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"CliAuth": {
"path": "ui/src/pages/CliAuth.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"Companies": {
"path": "ui/src/pages/Companies.tsx",
"components_imported": [
"button",
"dropdown-menu",
"input"
],
"component_count": 3
},
"CompanyAccess": {
"path": "ui/src/pages/CompanyAccess.tsx",
"components_imported": [
"badge",
"button",
"checkbox",
"dialog"
],
"component_count": 4
},
"CompanyExport": {
"path": "ui/src/pages/CompanyExport.tsx",
"components_imported": [
"EmptyState",
"MarkdownBody",
"PackageFileTree",
"PageSkeleton",
"button"
],
"component_count": 5
},
"CompanyImport": {
"path": "ui/src/pages/CompanyImport.tsx",
"components_imported": [
"AgentConfigForm",
"EmptyState",
"MarkdownBody",
"PackageFileTree",
"agent-config-defaults",
"agent-config-primitives",
"button"
],
"component_count": 7
},
"CompanyInvites": {
"path": "ui/src/pages/CompanyInvites.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"CompanySettings": {
"path": "ui/src/pages/CompanySettings.tsx",
"components_imported": [
"CompanyPatternIcon",
"agent-config-primitives",
"button"
],
"component_count": 3
},
"CompanySkills": {
"path": "ui/src/pages/CompanySkills.tsx",
"components_imported": [
"EmptyState",
"MarkdownBody",
"MarkdownEditor",
"PageSkeleton",
"button",
"dialog",
"input",
"textarea",
"tooltip"
],
"component_count": 9
},
"Costs": {
"path": "ui/src/pages/Costs.tsx",
"components_imported": [
"BillerSpendCard",
"BudgetIncidentCard",
"BudgetPolicyCard",
"EmptyState",
"FinanceBillerCard",
"FinanceKindCard",
"FinanceTimelineCard",
"Identity",
"PageSkeleton",
"PageTabBar",
"ProviderQuotaCard",
"StatusBadge",
"button",
"card",
"tabs"
],
"component_count": 15
},
"Dashboard": {
"path": "ui/src/pages/Dashboard.tsx",
"components_imported": [
"ActiveAgentsPanel",
"ActivityCharts",
"ActivityRow",
"EmptyState",
"Identity",
"MetricCard",
"PageSkeleton",
"StatusIcon"
],
"component_count": 8
},
"DesignGuide": {
"path": "ui/src/pages/DesignGuide.tsx",
"components_imported": [
"EmptyState",
"EntityRow",
"FilterBar",
"Identity",
"InlineEditor",
"IssueReferencePill",
"MetricCard",
"PageSkeleton",
"PriorityIcon",
"StatusBadge",
"StatusIcon",
"avatar",
"badge",
"breadcrumb",
"button",
"card",
"checkbox",
"collapsible",
"command",
"dialog",
"dropdown-menu",
"input",
"label",
"popover",
"scroll-area",
"select",
"separator",
"sheet",
"skeleton",
"tabs",
"textarea",
"tooltip"
],
"component_count": 32
},
"ExecutionWorkspaceDetail": {
"path": "ui/src/pages/ExecutionWorkspaceDetail.tsx",
"components_imported": [
"CopyText",
"ExecutionWorkspaceCloseDialog",
"IssuesList",
"PageTabBar",
"WorkspaceRuntimeControls",
"button",
"card",
"input",
"separator",
"tabs",
"textarea"
],
"component_count": 11
},
"GoalDetail": {
"path": "ui/src/pages/GoalDetail.tsx",
"components_imported": [
"EntityRow",
"GoalProperties",
"GoalTree",
"InlineEditor",
"PageSkeleton",
"StatusBadge",
"button",
"tabs"
],
"component_count": 8
},
"Goals": {
"path": "ui/src/pages/Goals.tsx",
"components_imported": [
"EmptyState",
"GoalTree",
"PageSkeleton",
"button"
],
"component_count": 4
},
"Inbox": {
"path": "ui/src/pages/Inbox.tsx",
"components_imported": [
"ApprovalPayload",
"EmptyState",
"IssueColumns",
"IssueFiltersPopover",
"IssueGroupHeader",
"IssueRow",
"PageSkeleton",
"PageTabBar",
"StatusBadge",
"StatusIcon",
"SwipeToArchive",
"button",
"dialog",
"input",
"popover",
"select",
"separator",
"tabs"
],
"component_count": 18
},
"InstanceAccess": {
"path": "ui/src/pages/InstanceAccess.tsx",
"components_imported": [
"button",
"checkbox"
],
"component_count": 2
},
"InstanceExperimentalSettings": {
"path": "ui/src/pages/InstanceExperimentalSettings.tsx",
"components_imported": [
"toggle-switch"
],
"component_count": 1
},
"InstanceGeneralSettings": {
"path": "ui/src/pages/InstanceGeneralSettings.tsx",
"components_imported": [
"ModeBadge",
"button",
"toggle-switch"
],
"component_count": 3
},
"InstanceSettings": {
"path": "ui/src/pages/InstanceSettings.tsx",
"components_imported": [
"EmptyState",
"badge",
"button",
"card"
],
"component_count": 4
},
"InviteLanding": {
"path": "ui/src/pages/InviteLanding.tsx",
"components_imported": [
"CompanyPatternIcon",
"button"
],
"component_count": 2
},
"InviteUxLab": {
"path": "ui/src/pages/InviteUxLab.tsx",
"components_imported": [
"CompanyPatternIcon",
"badge",
"button",
"card"
],
"component_count": 4
},
"IssueChatUxLab": {
"path": "ui/src/pages/IssueChatUxLab.tsx",
"components_imported": [
"IssueChatThread",
"badge",
"button",
"card"
],
"component_count": 4
},
"IssueDetail": {
"path": "ui/src/pages/IssueDetail.tsx",
"components_imported": [
"ApprovalCard",
"Identity",
"ImageGalleryModal",
"InlineEditor",
"IssueChatThread",
"IssueContinuationHandoff",
"IssueDocumentsSection",
"IssueProperties",
"IssueReferenceActivitySummary",
"IssueRelatedWorkPanel",
"IssueRunLedger",
"IssueWorkspaceCard",
"IssuesList",
"MarkdownEditor",
"PriorityIcon",
"ScrollToBottom",
"StatusIcon",
"button",
"popover",
"scroll-area",
"separator",
"sheet",
"skeleton",
"tabs"
],
"component_count": 24
},
"Issues": {
"path": "ui/src/pages/Issues.tsx",
"components_imported": [
"EmptyState",
"IssuesList"
],
"component_count": 2
},
"JoinRequestQueue": {
"path": "ui/src/pages/JoinRequestQueue.tsx",
"components_imported": [
"badge",
"button"
],
"component_count": 2
},
"MyIssues": {
"path": "ui/src/pages/MyIssues.tsx",
"components_imported": [
"EmptyState",
"EntityRow",
"PageSkeleton",
"StatusIcon"
],
"component_count": 4
},
"NewAgent": {
"path": "ui/src/pages/NewAgent.tsx",
"components_imported": [
"AgentConfigForm",
"ReportsToPicker",
"agent-config-defaults",
"agent-config-primitives",
"button",
"checkbox",
"popover"
],
"component_count": 7
},
"NotFound": {
"path": "ui/src/pages/NotFound.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"Org": {
"path": "ui/src/pages/Org.tsx",
"components_imported": [
"EmptyState",
"PageSkeleton",
"StatusBadge"
],
"component_count": 3
},
"OrgChart": {
"path": "ui/src/pages/OrgChart.tsx",
"components_imported": [
"AgentIconPicker",
"EmptyState",
"PageSkeleton",
"button"
],
"component_count": 4
},
"PluginManager": {
"path": "ui/src/pages/PluginManager.tsx",
"components_imported": [
"badge",
"button",
"card",
"dialog",
"input",
"label"
],
"component_count": 6
},
"PluginPage": {
"path": "ui/src/pages/PluginPage.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"PluginSettings": {
"path": "ui/src/pages/PluginSettings.tsx",
"components_imported": [
"JsonSchemaForm",
"PageTabBar",
"badge",
"button",
"card",
"separator",
"tabs"
],
"component_count": 7
},
"ProfileSettings": {
"path": "ui/src/pages/ProfileSettings.tsx",
"components_imported": [
"avatar",
"button",
"input",
"label"
],
"component_count": 4
},
"ProjectDetail": {
"path": "ui/src/pages/ProjectDetail.tsx",
"components_imported": [
"BudgetPolicyCard",
"InlineEditor",
"IssuesList",
"PageSkeleton",
"PageTabBar",
"ProjectProperties",
"ProjectWorkspacesContent",
"StatusBadge",
"button",
"tabs"
],
"component_count": 10
},
"ProjectWorkspaceDetail": {
"path": "ui/src/pages/ProjectWorkspaceDetail.tsx",
"components_imported": [
"PathInstructionsModal",
"WorkspaceRuntimeControls",
"button",
"separator"
],
"component_count": 4
},
"Projects": {
"path": "ui/src/pages/Projects.tsx",
"components_imported": [
"EmptyState",
"EntityRow",
"PageSkeleton",
"StatusBadge",
"button"
],
"component_count": 5
},
"RoutineDetail": {
"path": "ui/src/pages/RoutineDetail.tsx",
"components_imported": [
"AgentActionButtons",
"AgentIconPicker",
"EmptyState",
"InlineEntitySelector",
"LiveRunWidget",
"MarkdownEditor",
"PageSkeleton",
"RoutineRunVariablesDialog",
"RoutineVariablesEditor",
"ScheduleEditor",
"badge",
"button",
"collapsible",
"input",
"label",
"select",
"separator",
"tabs",
"toggle-switch"
],
"component_count": 19
},
"Routines": {
"path": "ui/src/pages/Routines.tsx",
"components_imported": [
"AgentIconPicker",
"EmptyState",
"InlineEntitySelector",
"IssuesList",
"MarkdownEditor",
"PageSkeleton",
"PageTabBar",
"RoutineRunVariablesDialog",
"RoutineVariablesEditor",
"button",
"card",
"collapsible",
"dialog",
"dropdown-menu",
"popover",
"select",
"tabs",
"toggle-switch"
],
"component_count": 18
},
"RunTranscriptUxLab": {
"path": "ui/src/pages/RunTranscriptUxLab.tsx",
"components_imported": [
"Identity",
"RunTranscriptView",
"StatusBadge",
"badge",
"button"
],
"component_count": 5
},
"UserProfile": {
"path": "ui/src/pages/UserProfile.tsx",
"components_imported": [
"EmptyState",
"PageSkeleton",
"StatusBadge",
"avatar"
],
"component_count": 4
},
"Workspaces": {
"path": "ui/src/pages/Workspaces.tsx",
"components_imported": [
"PageSkeleton",
"ProjectWorkspacesContent"
],
"component_count": 2
}
}
}

View File

@@ -0,0 +1,338 @@
{
"generated_at": "2026-04-21T00:00:00Z",
"repo_sha": "a26e1288b627e82c554445732c7d844648e6b5e1",
"story_files": {
"agent-management.stories.tsx": {
"path": "ui/storybook/stories/agent-management.stories.tsx",
"components_imported": [
"ActiveAgentsPanel",
"AgentActionButtons",
"AgentConfigForm",
"AgentIconPicker",
"AgentProperties",
"agent-config-defaults",
"agent-config-primitives",
"badge",
"button",
"card",
"select",
"separator"
],
"component_count": 12
},
"budget-finance.stories.tsx": {
"path": "ui/storybook/stories/budget-finance.stories.tsx",
"components_imported": [
"AccountingModelCard",
"BillerSpendCard",
"BudgetIncidentCard",
"BudgetSidebarMarker",
"ClaudeSubscriptionPanel",
"CodexSubscriptionPanel",
"FinanceBillerCard",
"FinanceKindCard",
"FinanceTimelineCard",
"ProviderQuotaCard",
"badge",
"card"
],
"component_count": 12
},
"chat-comments.stories.tsx": {
"path": "ui/storybook/stories/chat-comments.stories.tsx",
"components_imported": [
"CommentThread",
"InlineEntitySelector",
"IssueChatThread",
"MarkdownEditor",
"RunChatSurface",
"badge",
"card"
],
"component_count": 7
},
"control-plane-surfaces.stories.tsx": {
"path": "ui/storybook/stories/control-plane-surfaces.stories.tsx",
"components_imported": [
"ActivityRow",
"ApprovalCard",
"BudgetPolicyCard",
"Identity",
"IssueRow",
"PriorityIcon",
"StatusBadge",
"badge",
"card"
],
"component_count": 9
},
"data-viz-misc.stories.tsx": {
"path": "ui/storybook/stories/data-viz-misc.stories.tsx",
"components_imported": [
"ActivityCharts",
"AsciiArtAnimation",
"CompanyPatternIcon",
"EntityRow",
"FilterBar",
"KanbanBoard",
"LiveRunWidget",
"OnboardingWizard",
"PackageFileTree",
"PageSkeleton",
"StatusBadge",
"SwipeToArchive",
"badge",
"button",
"card"
],
"component_count": 15
},
"dialogs-modals.stories.tsx": {
"path": "ui/storybook/stories/dialogs-modals.stories.tsx",
"components_imported": [
"DocumentDiffModal",
"ExecutionWorkspaceCloseDialog",
"ImageGalleryModal",
"NewAgentDialog",
"NewGoalDialog",
"NewIssueDialog",
"NewProjectDialog",
"PathInstructionsModal",
"badge"
],
"component_count": 9
},
"forms-editors.stories.tsx": {
"path": "ui/storybook/stories/forms-editors.stories.tsx",
"components_imported": [
"EnvVarEditor",
"ExecutionParticipantPicker",
"InlineEditor",
"InlineEntitySelector",
"JsonSchemaForm",
"MarkdownBody",
"MarkdownEditor",
"ReportsToPicker",
"RoutineRunVariablesDialog",
"RoutineVariablesEditor",
"ScheduleEditor",
"badge",
"button"
],
"component_count": 13
},
"foundations.stories.tsx": {
"path": "ui/storybook/stories/foundations.stories.tsx",
"components_imported": [
"badge",
"button",
"card",
"checkbox",
"dialog",
"input",
"label",
"popover",
"select",
"separator",
"tabs",
"textarea",
"toggle-switch",
"tooltip"
],
"component_count": 14
},
"issue-management.stories.tsx": {
"path": "ui/storybook/stories/issue-management.stories.tsx",
"components_imported": [
"Identity",
"IssueColumns",
"IssueContinuationHandoff",
"IssueDocumentsSection",
"IssueFiltersPopover",
"IssueGroupHeader",
"IssueLinkQuicklook",
"IssueProperties",
"IssueRunLedger",
"IssueWorkspaceCard",
"IssuesList",
"IssuesQuicklook",
"PriorityIcon",
"StatusBadge",
"badge",
"button",
"card"
],
"component_count": 17
},
"navigation-layout.stories.tsx": {
"path": "ui/storybook/stories/navigation-layout.stories.tsx",
"components_imported": [
"BreadcrumbBar",
"CommandPalette",
"CompanyRail",
"CompanySwitcher",
"KeyboardShortcutsCheatsheet",
"MobileBottomNav",
"PageTabBar",
"Sidebar",
"SidebarAccountMenu",
"SidebarCompanyMenu",
"StatusBadge",
"badge",
"command",
"tabs"
],
"component_count": 14
},
"overview.stories.tsx": {
"path": "ui/storybook/stories/overview.stories.tsx",
"components_imported": [
"badge",
"card"
],
"component_count": 2
},
"projects-goals-workspaces.stories.tsx": {
"path": "ui/storybook/stories/projects-goals-workspaces.stories.tsx",
"components_imported": [
"GoalProperties",
"GoalTree",
"ProjectProperties",
"ProjectWorkspaceSummaryCard",
"ProjectWorkspacesContent",
"WorkspaceRuntimeControls",
"WorktreeBanner",
"badge",
"card"
],
"component_count": 9
},
"status-language.stories.tsx": {
"path": "ui/storybook/stories/status-language.stories.tsx",
"components_imported": [
"CopyText",
"EmptyState",
"Identity",
"MetricCard",
"PriorityIcon",
"QuotaBar",
"StatusBadge",
"card"
],
"component_count": 8
},
"ux-labs.stories.tsx": {
"path": "ui/storybook/stories/ux-labs.stories.tsx",
"components_imported": [],
"component_count": 0
}
},
"covered_components_count": 104,
"covered_components": [
"AccountingModelCard",
"ActiveAgentsPanel",
"ActivityCharts",
"ActivityRow",
"AgentActionButtons",
"AgentConfigForm",
"AgentIconPicker",
"AgentProperties",
"ApprovalCard",
"AsciiArtAnimation",
"BillerSpendCard",
"BreadcrumbBar",
"BudgetIncidentCard",
"BudgetPolicyCard",
"BudgetSidebarMarker",
"ClaudeSubscriptionPanel",
"CodexSubscriptionPanel",
"CommandPalette",
"CommentThread",
"CompanyPatternIcon",
"CompanyRail",
"CompanySwitcher",
"CopyText",
"DocumentDiffModal",
"EmptyState",
"EntityRow",
"EnvVarEditor",
"ExecutionParticipantPicker",
"ExecutionWorkspaceCloseDialog",
"FilterBar",
"FinanceBillerCard",
"FinanceKindCard",
"FinanceTimelineCard",
"GoalProperties",
"GoalTree",
"Identity",
"ImageGalleryModal",
"InlineEditor",
"InlineEntitySelector",
"IssueChatThread",
"IssueColumns",
"IssueContinuationHandoff",
"IssueDocumentsSection",
"IssueFiltersPopover",
"IssueGroupHeader",
"IssueLinkQuicklook",
"IssueProperties",
"IssueRow",
"IssueRunLedger",
"IssueWorkspaceCard",
"IssuesList",
"IssuesQuicklook",
"JsonSchemaForm",
"KanbanBoard",
"KeyboardShortcutsCheatsheet",
"LiveRunWidget",
"MarkdownBody",
"MarkdownEditor",
"MetricCard",
"MobileBottomNav",
"NewAgentDialog",
"NewGoalDialog",
"NewIssueDialog",
"NewProjectDialog",
"OnboardingWizard",
"PackageFileTree",
"PageSkeleton",
"PageTabBar",
"PathInstructionsModal",
"PriorityIcon",
"ProjectProperties",
"ProjectWorkspaceSummaryCard",
"ProjectWorkspacesContent",
"ProviderQuotaCard",
"QuotaBar",
"ReportsToPicker",
"RoutineRunVariablesDialog",
"RoutineVariablesEditor",
"RunChatSurface",
"ScheduleEditor",
"Sidebar",
"SidebarAccountMenu",
"SidebarCompanyMenu",
"StatusBadge",
"SwipeToArchive",
"WorkspaceRuntimeControls",
"WorktreeBanner",
"agent-config-defaults",
"agent-config-primitives",
"badge",
"button",
"card",
"checkbox",
"command",
"dialog",
"input",
"label",
"popover",
"select",
"separator",
"tabs",
"textarea",
"toggle-switch",
"tooltip"
],
"covered_components_note": "Computed by parsing 'from \"@/components/...\"' and relative imports across all .stories.tsx files. Coverage is set membership \u2014 a component appears once if any story imports it, regardless of how many variants/states are rendered."
}

View File

@@ -0,0 +1,21 @@
# ActivityCharts
`ui/src/components/ActivityCharts.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 274 lines
- **Sibling exports:** ChartCard, IssueStatusChart, PriorityChart, RunActivityChart, SuccessRateChart
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `Dashboard`

View File

@@ -0,0 +1,40 @@
# AgentConfigForm
`ui/src/components/AgentConfigForm.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 5 imports (3 pages, 0 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 1403 lines
## Props
### `AgentConfigFormProps`
```ts
adapterModels?: AdapterModel[];
onDirtyChange?: (dirty: boolean) => void;
onSaveActionChange?: (save: (() => void) | null) => void;
onCancelActionChange?: (cancel: (() => void) | null) => void;
hideInlineSave?: boolean;
showAdapterTypeField?: boolean;
showAdapterTestEnvironmentButton?: boolean;
showCreateRunPolicySection?: boolean;
hideInstructionsFile?: boolean;
/** Hide the prompt template field from the Identity section (used when it's shown in a separate Prompts tab). */
hidePromptTemplate?: boolean;
/** "cards" renders each section as heading + bordered card (for settings pages). Default: "inline" (border-b dividers). */
sectionLayout?: "inline" | "cards";
```
## Composes
- **Primitives:** [button](./Button.md), [popover](./Popover.md)
## Used by
- **Pages:** `AgentDetail`, `CompanyImport`, `NewAgent`

View File

@@ -0,0 +1,38 @@
# AgentIconPicker
`ui/src/components/AgentIconPicker.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 13 imports (4 pages, 9 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 81 lines
- **Sibling exports:** AgentIcon
## Props
### `AgentIconProps`
```ts
icon: string | null | undefined;
className?: string;
```
### `AgentIconPickerProps`
```ts
value: string | null | undefined;
onChange: (icon: string) => void;
children: React.ReactNode;
```
## Composes
- **Primitives:** [input](./Input.md), [popover](./Popover.md)
## Used by
- **Pages:** `AgentDetail`, `OrgChart`, `RoutineDetail`, `Routines`

View File

@@ -0,0 +1,24 @@
# ApprovalCard
`ui/src/components/ApprovalCard.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 153 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [badge](./Badge.md), [button](./Button.md)
## Used by
- **Pages:** `Approvals`, `IssueDetail`

View File

@@ -0,0 +1,21 @@
# ApprovalPayload
`ui/src/components/ApprovalPayload.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 4 imports (2 pages, 2 components)
- **Storybook:** no
- **File size:** 248 lines
- **Sibling exports:** ApprovalPayloadRenderer, BoardApprovalPayload, BudgetOverridePayload, CeoStrategyPayload, HireAgentPayload
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `ApprovalDetail`, `Inbox`

View File

@@ -0,0 +1,22 @@
# Avatar
`ui/src/components/ui/avatar.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 7 imports (3 pages, 4 components)
- **Storybook:** no
- **File size:** 108 lines
- **Sibling exports:** AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `DesignGuide`, `ProfileSettings`, `UserProfile`
- **Components:** `CommentThread`, `Identity`, `IssueChatThread`, `SidebarAccountMenu`

View File

@@ -0,0 +1,26 @@
# Badge
`ui/src/components/ui/badge.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 18 imports (11 pages, 7 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 49 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Variants (CVA)
- **variant**: `default`, `secondary`, `destructive`, `outline`, `ghost`, `link`
## Used by
- **Pages:** `AdapterManager`, `CompanyAccess`, `DesignGuide`, `InstanceSettings`, `InviteUxLab` … (+6 more)
- **Components:** `ApprovalCard`, `BudgetIncidentCard`, `FilterBar`, `FinanceTimelineCard`, `IssueFiltersPopover` … (+2 more)

View File

@@ -0,0 +1,24 @@
# BudgetPolicyCard
`ui/src/components/BudgetPolicyCard.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (3 pages, 0 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 220 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [button](./Button.md), [card](./Card.md), [input](./Input.md)
## Used by
- **Pages:** `AgentDetail`, `Costs`, `ProjectDetail`

View File

@@ -0,0 +1,27 @@
# Button
`ui/src/components/ui/button.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 81 imports (41 pages, 38 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 71 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Variants (CVA)
- **variant**: `default`, `destructive`, `outline`, `secondary`, `ghost`, `link`
- **size**: `default`, `xs`, `sm`, `lg`, `icon`, `icon-xs`, `icon-sm`, `icon-lg`
## Used by
- **Pages:** `AdapterManager`, `AgentDetail`, `Agents`, `ApprovalDetail`, `Auth` … (+36 more)
- **Components:** `AgentActionButtons`, `AgentConfigForm`, `ApprovalCard`, `BreadcrumbBar`, `BudgetIncidentCard` … (+33 more)

View File

@@ -0,0 +1,22 @@
# Card
`ui/src/components/ui/card.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 18 imports (10 pages, 8 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 93 lines
- **Sibling exports:** CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AdapterManager`, `Costs`, `DesignGuide`, `ExecutionWorkspaceDetail`, `InstanceSettings` … (+5 more)
- **Components:** `AccountingModelCard`, `BillerSpendCard`, `BudgetIncidentCard`, `BudgetPolicyCard`, `FinanceBillerCard` … (+3 more)

View File

@@ -0,0 +1,21 @@
# Checkbox
`ui/src/components/ui/checkbox.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 6 imports (4 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 33 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `CompanyAccess`, `DesignGuide`, `InstanceAccess`, `NewAgent`
- **Components:** `IssueFiltersPopover`, `JsonSchemaForm`

View File

@@ -0,0 +1,22 @@
# Collapsible
`ui/src/components/ui/collapsible.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 8 imports (4 pages, 4 components)
- **Storybook:** no
- **File size:** 34 lines
- **Sibling exports:** CollapsibleContent, CollapsibleTrigger
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `DesignGuide`, `RoutineDetail`, `Routines`
- **Components:** `IssuesList`, `RoutineVariablesEditor`, `SidebarAgents`, `SidebarProjects`

View File

@@ -0,0 +1,28 @@
# CompanyPatternIcon
`ui/src/components/CompanyPatternIcon.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 4 imports (3 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 218 lines
## Props
### `CompanyPatternIconProps`
```ts
companyName: string;
logoUrl?: string | null;
brandColor?: string | null;
className?: string;
logoFit?: "cover" | "contain";
```
## Used by
- **Pages:** `CompanySettings`, `InviteLanding`, `InviteUxLab`

View File

@@ -0,0 +1,32 @@
# CopyText
`ui/src/components/CopyText.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 88 lines
## Props
### `CopyTextProps`
```ts
text: string;
/** What to display. Defaults to `text`. */
children?: React.ReactNode;
containerClassName?: string;
className?: string;
ariaLabel?: string;
title?: string;
/** Tooltip message shown after copying. Default: "Copied!" */
copiedLabel?: string;
```
## Used by
- **Pages:** `AgentDetail`, `ExecutionWorkspaceDetail`

View File

@@ -0,0 +1,26 @@
# Dialog
`ui/src/components/ui/dialog.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 21 imports (7 pages, 14 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 157 lines
- **Sibling exports:** DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [button](./Button.md)
## Used by
- **Pages:** `AdapterManager`, `CompanyAccess`, `CompanySkills`, `DesignGuide`, `Inbox` … (+2 more)
- **Components:** `DocumentDiffModal`, `IssueChatThread`, `KeyboardShortcutsCheatsheet`, `NewAgentDialog`, `NewGoalDialog` … (+9 more)

View File

@@ -0,0 +1,22 @@
# DropdownMenu
`ui/src/components/ui/dropdown-menu.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 8 imports (3 pages, 5 components)
- **Storybook:** no
- **File size:** 258 lines
- **Sibling exports:** DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `Companies`, `DesignGuide`, `Routines`
- **Components:** `CompanySwitcher`, `IssueChatThread`, `IssueColumns`, `IssueDocumentsSection`, `SidebarCompanyMenu`

View File

@@ -0,0 +1,31 @@
# EmptyState
`ui/src/components/EmptyState.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 20 imports (19 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 28 lines
## Props
### `EmptyStateProps`
```ts
icon: LucideIcon;
message: string;
action?: string;
onAction?: () => void;
```
## Composes
- **Primitives:** [button](./Button.md)
## Used by
- **Pages:** `Activity`, `Agents`, `CompanyExport`, `CompanyImport`, `CompanySkills` … (+14 more)

View File

@@ -0,0 +1,32 @@
# EntityRow
`ui/src/components/EntityRow.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 6 imports (6 pages, 0 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 70 lines
## Props
### `EntityRowProps`
```ts
leading?: ReactNode;
identifier?: string;
title: string;
subtitle?: string;
trailing?: ReactNode;
selected?: boolean;
to?: string;
onClick?: () => void;
className?: string;
```
## Used by
- **Pages:** `AgentDetail`, `Agents`, `DesignGuide`, `GoalDetail`, `MyIssues` … (+1 more)

View File

@@ -0,0 +1,32 @@
# Identity
`ui/src/components/Identity.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 19 imports (7 pages, 12 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 40 lines
## Props
### `IdentityProps`
```ts
name: string;
avatarUrl?: string | null;
initials?: string;
size?: IdentitySize;
className?: string;
```
## Composes
- **Primitives:** [avatar](./Avatar.md)
## Used by
- **Pages:** `AgentDetail`, `ApprovalDetail`, `Costs`, `Dashboard`, `DesignGuide` … (+2 more)

View File

@@ -0,0 +1,34 @@
# InlineEditor
`ui/src/components/InlineEditor.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 6 imports (4 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 310 lines
## Props
### `InlineEditorProps`
```ts
value: string;
onSave: (value: string) => void | Promise<unknown>;
as?: "h1" | "h2" | "p" | "span";
className?: string;
placeholder?: string;
multiline?: boolean;
imageUploadHandler?: (file: File) => Promise<string>;
/** Called when a non-image file is dropped onto the editor. */
onDropFile?: (file: File) => Promise<void>;
mentions?: MentionOption[];
nullable?: boolean;
```
## Used by
- **Pages:** `DesignGuide`, `GoalDetail`, `IssueDetail`, `ProjectDetail`

View File

@@ -0,0 +1,43 @@
# InlineEntitySelector
`ui/src/components/InlineEntitySelector.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 8 imports (2 pages, 4 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 215 lines
## Props
### `InlineEntitySelectorProps`
```ts
value: string;
options: InlineEntityOption[];
placeholder: string;
noneLabel: string;
searchPlaceholder: string;
emptyMessage: string;
onChange: (id: string) => void;
onConfirm?: () => void;
className?: string;
renderTriggerValue?: (option: InlineEntityOption | null) => ReactNode;
renderOption?: (option: InlineEntityOption, isSelected: boolean) => ReactNode;
recentOptionIds?: string[];
/** Skip the Portal so the popover stays in the DOM tree (fixes scroll inside Dialogs). */
disablePortal?: boolean;
/** Open the popover when the trigger receives keyboard/programmatic focus. */
openOnFocus?: boolean;
```
## Composes
- **Primitives:** [popover](./Popover.md)
## Used by
- **Pages:** `RoutineDetail`, `Routines`

View File

@@ -0,0 +1,21 @@
# Input
`ui/src/components/ui/input.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 20 imports (10 pages, 10 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 22 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AdapterManager`, `AgentDetail`, `Companies`, `CompanySkills`, `DesignGuide` … (+5 more)
- **Components:** `AgentIconPicker`, `BudgetIncidentCard`, `BudgetPolicyCard`, `IssueDocumentsSection`, `IssueFiltersPopover` … (+5 more)

View File

@@ -0,0 +1,73 @@
# IssueChatThread
`ui/src/components/IssueChatThread.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 4 imports (2 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 2399 lines
## Props
### `IssueChatComposerProps`
```ts
onImageUpload?: (file: File) => Promise<string>;
onAttachImage?: (file: File) => Promise<void>;
draftKey?: string;
enableReassign?: boolean;
reassignOptions?: InlineEntityOption[];
currentAssigneeValue?: string;
suggestedAssigneeValue?: string;
mentions?: MentionOption[];
agentMap?: Map<string, Agent>;
composerDisabledReason?: string | null;
issueStatus?: string;
```
### `IssueChatThreadProps`
```ts
comments: IssueChatComment[];
feedbackVotes?: FeedbackVote[];
feedbackDataSharingPreference?: FeedbackDataSharingPreference;
feedbackTermsUrl?: string | null;
linkedRuns?: IssueChatLinkedRun[];
timelineEvents?: IssueTimelineEvent[];
liveRuns?: LiveRunForIssue[];
activeRun?: ActiveRunForIssue | null;
blockedBy?: IssueRelationIssueSummary[];
companyId?: string | null;
projectId?: string | null;
issueStatus?: string;
agentMap?: Map<string, Agent>;
currentUserId?: string | null;
userLabelMap?: ReadonlyMap<string, string> | null;
userProfileMap?: ReadonlyMap<string, CompanyUserProfile> | null;
onVote?: (
commentId: string,
vote: FeedbackVoteValue,
options?: { allowSharing?: boolean; reason?: string
```
### `IssueChatErrorBoundaryProps`
```ts
resetKey: string;
messages: readonly ThreadMessage[];
emptyMessage: string;
variant: "full" | "embedded";
children: ReactNode;
```
## Composes
- **Primitives:** [avatar](./Avatar.md), [button](./Button.md), [dialog](./Dialog.md), `dropdown-menu`, [popover](./Popover.md), [textarea](./Textarea.md), [tooltip](./Tooltip.md)
## Used by
- **Pages:** `IssueChatUxLab`, `IssueDetail`

View File

@@ -0,0 +1,24 @@
# IssueFiltersPopover
`ui/src/components/IssueFiltersPopover.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (1 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 366 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [badge](./Badge.md), [button](./Button.md), [checkbox](./Checkbox.md), [input](./Input.md), [popover](./Popover.md)
## Used by
- **Pages:** `Inbox`

View File

@@ -0,0 +1,25 @@
# IssueLinkQuicklook
`ui/src/components/IssueLinkQuicklook.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (0 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 182 lines
- **Sibling exports:** IssueQuicklookCard
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [popover](./Popover.md)
- **Composites:** [StatusIcon](./StatusIcon.md)
## Used by

View File

@@ -0,0 +1,20 @@
# IssueReferencePill
`ui/src/components/IssueReferencePill.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 4 imports (1 pages, 3 components)
- **Storybook:** no
- **File size:** 56 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `DesignGuide`

View File

@@ -0,0 +1,38 @@
# IssueRow
`ui/src/components/IssueRow.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (1 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 169 lines
## Props
### `IssueRowProps`
```ts
issue: Issue;
issueLinkState?: unknown;
selected?: boolean;
mobileLeading?: ReactNode;
desktopMetaLeading?: ReactNode;
desktopLeadingSpacer?: boolean;
mobileMeta?: ReactNode;
desktopTrailing?: ReactNode;
trailingMeta?: ReactNode;
titleSuffix?: ReactNode;
unreadState?: UnreadState | null;
onMarkRead?: () => void;
onArchive?: () => void;
archiveDisabled?: boolean;
className?: string;
```
## Used by
- **Pages:** `Inbox`

View File

@@ -0,0 +1,41 @@
# IssueWorkspaceCard
`ui/src/components/IssueWorkspaceCard.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (1 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 522 lines
## Props
### `IssueWorkspaceCardProps`
```ts
issue: Omit<
Pick<
Issue,
| "companyId"
| "projectId"
| "projectWorkspaceId"
| "executionWorkspaceId"
| "executionWorkspacePreference"
| "executionWorkspaceSettings"
>,
"companyId"
> & {
companyId: string | null;
currentExecutionWorkspace?: ExecutionWorkspace | null;
```
## Composes
- **Primitives:** [button](./Button.md), [skeleton](./Skeleton.md)
## Used by
- **Pages:** `IssueDetail`

View File

@@ -0,0 +1,45 @@
# IssuesList
`ui/src/components/IssuesList.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 6 imports (5 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 1170 lines
## Props
### `IssuesListProps`
```ts
issues: Issue[];
isLoading?: boolean;
error?: Error | null;
agents?: Agent[];
projects?: ProjectOption[];
liveIssueIds?: Set<string>;
projectId?: string;
viewStateKey: string;
issueLinkState?: unknown;
initialAssignees?: string[];
initialWorkspaces?: string[];
initialSearch?: string;
searchFilters?: Omit<IssueListRequestFilters, "q" | "projectId" | "limit" | "includeRoutineExecutions">;
baseCreateIssueDefaults?: Record<string, unknown>;
createIssueLabel?: string;
enableRoutineVisibilityFilter?: boolean;
onSearchChange?: (search: string) => void;
onUpdateIssue: (id: string, data: Record<string, unknown>) => void;
```
## Composes
- **Primitives:** [button](./Button.md), [collapsible](./Collapsible.md), [input](./Input.md), [popover](./Popover.md)
## Used by
- **Pages:** `ExecutionWorkspaceDetail`, `IssueDetail`, `Issues`, `ProjectDetail`, `Routines`

View File

@@ -0,0 +1,21 @@
# Label
`ui/src/components/ui/label.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 8 imports (5 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 23 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AdapterManager`, `DesignGuide`, `PluginManager`, `ProfileSettings`, `RoutineDetail`
- **Components:** `JsonSchemaForm`, `RoutineRunVariablesDialog`, `RoutineVariablesEditor`

View File

@@ -0,0 +1,32 @@
# MarkdownBody
`ui/src/components/MarkdownBody.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 11 imports (5 pages, 6 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 326 lines
## Props
### `MarkdownBodyProps`
```ts
children: string;
className?: string;
style?: React.CSSProperties;
softBreaks?: boolean;
linkIssueReferences?: boolean;
/** Optional resolver for relative image paths (e.g. within export packages) */
resolveImageSrc?: (src: string) => string | null;
/** Called when a user clicks an inline image */
onImageClick?: (src: string) => void;
```
## Used by
- **Pages:** `AgentDetail`, `ApprovalDetail`, `CompanyExport`, `CompanyImport`, `CompanySkills`

View File

@@ -0,0 +1,39 @@
# MarkdownEditor
`ui/src/components/MarkdownEditor.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 16 imports (5 pages, 9 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 1204 lines
## Props
### `MarkdownEditorProps`
```ts
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
contentClassName?: string;
onBlur?: () => void;
imageUploadHandler?: (file: File) => Promise<string>;
/** Called when a non-image file is dropped onto the editor (e.g. .zip). */
onDropFile?: (file: File) => Promise<void>;
bordered?: boolean;
/** List of mentionable entities. Enables @-mention autocomplete. */
mentions?: MentionOption[];
/** Called on Cmd/Ctrl+Enter */
onSubmit?: () => void;
/** Render the rich editor without allowing edits. */
readOnly?: boolean;
```
## Used by
- **Pages:** `AgentDetail`, `CompanySkills`, `IssueDetail`, `RoutineDetail`, `Routines`

View File

@@ -0,0 +1,21 @@
# PackageFileTree
`ui/src/components/PackageFileTree.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (3 pages, 0 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 327 lines
- **Sibling exports:** FRONTMATTER_FIELD_LABELS
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `CompanyExport`, `CompanyImport`

View File

@@ -0,0 +1,36 @@
# PageSkeleton
`ui/src/components/PageSkeleton.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 23 imports (22 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 181 lines
## Props
### `PageSkeletonProps`
```ts
variant?:
| "list"
| "issues-list"
| "detail"
| "dashboard"
| "approvals"
| "costs"
| "inbox"
| "org-chart";
```
## Composes
- **Primitives:** [skeleton](./Skeleton.md)
## Used by
- **Pages:** `Activity`, `AgentDetail`, `Agents`, `ApprovalDetail`, `Approvals` … (+17 more)

View File

@@ -0,0 +1,32 @@
# PageTabBar
`ui/src/components/PageTabBar.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 10 imports (9 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 46 lines
## Props
### `PageTabBarProps`
```ts
items: PageTabItem[];
value?: string;
onValueChange?: (value: string) => void;
align?: "center" | "start";
```
## Composes
- **Primitives:** [tabs](./Tabs.md)
## Used by
- **Pages:** `AgentDetail`, `Agents`, `Approvals`, `Costs`, `ExecutionWorkspaceDetail` … (+4 more)
- **Components:** `CompanySettingsNav`

View File

@@ -0,0 +1,30 @@
# PathInstructionsModal
`ui/src/components/PathInstructionsModal.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 12 imports (2 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 144 lines
- **Sibling exports:** ChoosePathButton
## Props
### `PathInstructionsModalProps`
```ts
open: boolean;
onOpenChange: (open: boolean) => void;
```
## Composes
- **Primitives:** [dialog](./Dialog.md)
## Used by
- **Pages:** `AdapterManager`, `ProjectWorkspaceDetail`

View File

@@ -0,0 +1,22 @@
# Popover
`ui/src/components/ui/popover.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 27 imports (6 pages, 20 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 89 lines
- **Sibling exports:** PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `DesignGuide`, `Inbox`, `IssueDetail`, `NewAgent` … (+1 more)
- **Components:** `AgentConfigForm`, `AgentIconPicker`, `ExecutionParticipantPicker`, `GoalProperties`, `InlineEntitySelector` … (+15 more)

View File

@@ -0,0 +1,31 @@
# PriorityIcon
`ui/src/components/PriorityIcon.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 5 imports (2 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 78 lines
## Props
### `PriorityIconProps`
```ts
priority: string;
onChange?: (priority: string) => void;
className?: string;
showLabel?: boolean;
```
## Composes
- **Primitives:** [button](./Button.md), [popover](./Popover.md)
## Used by
- **Pages:** `DesignGuide`, `IssueDetail`

View File

@@ -0,0 +1,24 @@
# RoutineRunVariablesDialog
`ui/src/components/RoutineRunVariablesDialog.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 519 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [button](./Button.md), [dialog](./Dialog.md), [input](./Input.md), [label](./Label.md), [select](./Select.md), [textarea](./Textarea.md)
## Used by
- **Pages:** `RoutineDetail`, `Routines`

View File

@@ -0,0 +1,32 @@
# RunTranscriptView
`ui/src/components/transcript/RunTranscriptView.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** no
- **File size:** 1446 lines
## Props
### `RunTranscriptViewProps`
```ts
entries: TranscriptEntry[];
mode?: TranscriptMode;
density?: TranscriptDensity;
limit?: number;
streaming?: boolean;
collapseStdout?: boolean;
emptyMessage?: string;
className?: string;
thinkingClassName?: string;
```
## Used by
- **Pages:** `AgentDetail`, `RunTranscriptUxLab`

View File

@@ -0,0 +1,22 @@
# ScrollArea
`ui/src/components/ui/scroll-area.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** no
- **File size:** 57 lines
- **Sibling exports:** ScrollBar
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `DesignGuide`, `IssueDetail`
- **Components:** `PropertiesPanel`

View File

@@ -0,0 +1,22 @@
# Select
`ui/src/components/ui/select.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 10 imports (5 pages, 5 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 189 lines
- **Sibling exports:** SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `Activity`, `DesignGuide`, `Inbox`, `RoutineDetail`, `Routines`
- **Components:** `DocumentDiffModal`, `JsonSchemaForm`, `RoutineRunVariablesDialog`, `RoutineVariablesEditor`, `ScheduleEditor`

View File

@@ -0,0 +1,21 @@
# Separator
`ui/src/components/ui/separator.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 11 imports (7 pages, 4 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 29 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `DesignGuide`, `ExecutionWorkspaceDetail`, `Inbox`, `IssueDetail`, `PluginSettings` … (+2 more)
- **Components:** `AgentProperties`, `GoalProperties`, `IssueProperties`, `ProjectProperties`

View File

@@ -0,0 +1,33 @@
# SidebarNavItem
`ui/src/components/SidebarNavItem.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (0 pages, 3 components)
- **Storybook:** no
- **File size:** 95 lines
## Props
### `SidebarNavItemProps`
```ts
to: string;
label: string;
icon: LucideIcon;
end?: boolean;
className?: string;
badge?: number;
badgeTone?: "default" | "danger";
textBadge?: string;
textBadgeTone?: "default" | "amber";
alert?: boolean;
liveCount?: number;
```
## Used by

View File

@@ -0,0 +1,21 @@
# Skeleton
`ui/src/components/ui/skeleton.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 6 imports (3 pages, 3 components)
- **Storybook:** no
- **File size:** 14 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `DesignGuide`, `IssueDetail`
- **Components:** `IssueWorkspaceCard`, `PageSkeleton`, `ProviderQuotaCard`

View File

@@ -0,0 +1,20 @@
# StatusBadge
`ui/src/components/StatusBadge.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 19 imports (12 pages, 7 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 16 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `Agents`, `ApprovalDetail`, `Costs`, `DesignGuide` … (+7 more)

View File

@@ -0,0 +1,32 @@
# StatusIcon
`ui/src/components/StatusIcon.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 14 imports (5 pages, 9 components)
- **Storybook:** no
- **File size:** 72 lines
## Props
### `StatusIconProps`
```ts
status: string;
onChange?: (status: string) => void;
className?: string;
showLabel?: boolean;
```
## Composes
- **Primitives:** [button](./Button.md), [popover](./Popover.md)
## Used by
- **Pages:** `Dashboard`, `DesignGuide`, `Inbox`, `IssueDetail`, `MyIssues`
- **Components:** `IssueLinkQuicklook` … (+8 more)

View File

@@ -0,0 +1,27 @@
# Tabs
`ui/src/components/ui/tabs.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 15 imports (13 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 90 lines
- **Sibling exports:** TabsContent, TabsList, TabsTrigger
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Variants (CVA)
- **variant**: `default`, `line`
## Used by
- **Pages:** `AgentDetail`, `Agents`, `Approvals`, `Costs`, `DesignGuide` … (+8 more)
- **Components:** `CompanySettingsNav`, `PageTabBar`

View File

@@ -0,0 +1,21 @@
# Textarea
`ui/src/components/ui/textarea.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 9 imports (4 pages, 5 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 19 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `ApprovalDetail`, `CompanySkills`, `DesignGuide`, `ExecutionWorkspaceDetail`
- **Components:** `IssueChatThread`, `JsonSchemaForm`, `OutputFeedbackButtons`, `RoutineRunVariablesDialog`, `RoutineVariablesEditor`

View File

@@ -0,0 +1,21 @@
# ToggleSwitch
`ui/src/components/ui/toggle-switch.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 8 imports (5 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 60 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `InstanceExperimentalSettings`, `InstanceGeneralSettings`, `RoutineDetail`, `Routines`
- **Components:** `NewIssueDialog`, `ProjectProperties`, `agent-config-primitives`

View File

@@ -0,0 +1,22 @@
# Tooltip
`ui/src/components/ui/tooltip.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 11 imports (3 pages, 7 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 58 lines
- **Sibling exports:** TooltipContent, TooltipProvider, TooltipTrigger
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `CompanySkills`, `DesignGuide`
- **Components:** `CompanyRail`, `IssueChatThread`, `IssueColumns`, `NewProjectDialog`, `ProjectProperties` … (+2 more)

View File

@@ -0,0 +1,38 @@
# WorkspaceRuntimeControls
`ui/src/components/WorkspaceRuntimeControls.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 454 lines
## Props
### `WorkspaceRuntimeControlsProps`
```ts
sections: WorkspaceRuntimeControlSections;
items?: never;
isPending?: boolean;
pendingRequest?: WorkspaceRuntimeControlRequest | null;
serviceEmptyMessage?: string;
jobEmptyMessage?: string;
emptyMessage?: never;
disabledHint?: string | null;
onAction: (request: WorkspaceRuntimeControlRequest) => void;
className?: string;
square?: boolean;
```
## Composes
- **Primitives:** [button](./Button.md)
## Used by
- **Pages:** `ExecutionWorkspaceDetail`, `ProjectWorkspaceDetail`

View File

@@ -0,0 +1,25 @@
# agent-config-primitives
`ui/src/components/agent-config-primitives.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 19 imports (4 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 464 lines
- **Sibling exports:** AutoExpandTextarea, ChoosePathButton, CollapsibleSection, DraftInput, DraftNumberInput, DraftTextarea
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [button](./Button.md), [dialog](./Dialog.md), `toggle-switch`, [tooltip](./Tooltip.md)
## Used by
- **Pages:** `AgentDetail`, `CompanyImport`, `CompanySettings`, `NewAgent`

View File

@@ -0,0 +1,393 @@
# Components Review
- **Generated:** 2026-04-21
- **Repo SHA:** a26e1288b627e82c554445732c7d844648e6b5e1
- **Inventory:** [index.md](./index.md)
- **Token drift feeding components:** [../tokens/tokens-review.md](../tokens/tokens-review.md)
- **Pattern docs drawn from this review:** [../patterns/index.md](../patterns/index.md)
---
## How to read this document
This is a **persistent backlog**, not just a snapshot review. It captures opportunities identified during DS extraction. **All items are intentionally deferred** per the 2026-04-21 decision: no component merges, no renames, no file-casing changes were made. Address opportunistically when the relevant code is touched for other reasons, or schedule as standalone follow-up projects.
Each entry below includes:
- A **problem statement** — what was observed and why it might matter.
- **Affected files** — so the next developer doesn't have to re-trace.
- A **suggested resolution** — or "needs discussion" when the right call is not obvious.
Entries are ordered by expected value, not by stage. Sections are self-contained — you can pick one up cold without reading the rest.
---
## Likely duplicates
Pairs or families whose names, compositions, or role overlap enough that they may be consolidatable. None of these are automatic merges — each is a judgment call. Items 17 and 9 are documented as single patterns in [../patterns/](../patterns/) so the family is visible to future pattern extraction; this section captures the consolidation opportunity.
### 1. Entity-creation dialog family: `NewAgentDialog` / `NewGoalDialog` / `NewIssueDialog` / `NewProjectDialog`
Four parallel "create new X" dialogs, each used 12 times, each a composite that wraps a form-in-a-dialog. Strong candidate for a single generic `NewEntityDialog` or a `useNewEntityForm` hook plus entity-specific field sets.
| File | Uses |
|---|---|
| `ui/src/components/NewAgentDialog.tsx` | 1 |
| `ui/src/components/NewGoalDialog.tsx` | 1 |
| `ui/src/components/NewIssueDialog.tsx` | 2 |
| `ui/src/components/NewProjectDialog.tsx` | 1 |
**Verify:** open the four files and diff them. If ≥60% body overlap, consolidate. Impact on Stage 4: otherwise a "New-entity dialog" pattern would be proposed with 4 instances, which is better represented as a single component.
### 2. Properties-panel family: `AgentProperties` / `GoalProperties` / `IssueProperties` / `ProjectProperties` (+ generic `PropertiesPanel`)
**Problem.** Four entity-specific property-panel components sit next to a generic `PropertiesPanel`. It is unclear whether `PropertiesPanel` **composes** the four (i.e., hosts their body via a context slot — which is what the 29-line `PropertiesPanel.tsx` appears to do) or whether the four **duplicate** the outer-chrome work that `PropertiesPanel` could own. `AgentProperties` is also unused in production (Storybook-only).
**Affected files.**
| File | Uses | Notes |
|---|---|---|
| `ui/src/components/PropertiesPanel.tsx` | 1 | 29-line generic chrome; reads `panelContent` from `usePanel()` context |
| `ui/src/components/IssueProperties.tsx` | 2 | 1370-line entity-specific body |
| `ui/src/components/GoalProperties.tsx` | 1 | Entity-specific body |
| `ui/src/components/ProjectProperties.tsx` | 1 | 1140-line entity-specific body |
| `ui/src/components/AgentProperties.tsx` | **0** | Storybook-only — see §Unused components |
**Suggested resolution: needs discussion.** Per the 2026-04-21 decision, the open question of "composes vs duplicates" is **flagged but not resolved**. Reading the source suggests `PropertiesPanel` is the outer slot and the four bodies are what gets passed into it — so not a literal duplicate. But the four bodies each re-roll section headers, separators, save-state plumbing, and field layout; those might be factor-able into a shared `<PropertiesPanelBody>` helper. Open `IssueProperties.tsx` and `ProjectProperties.tsx` side-by-side before making any call. Documented as-is in [../patterns/entity-properties-panel.md](../patterns/entity-properties-panel.md).
### 3. Subscription panel pair: `ClaudeSubscriptionPanel` ↔ `CodexSubscriptionPanel`
**Problem.** Two components with parallel names, parallel props shape (both accept `windows: QuotaWindow[]` + optional `source` / `error`), rendering ordered subscription-quota windows for a single provider. Both used exactly once — always dispatched from `ProviderQuotaCard`. When a third vendor (Gemini? Cursor?) is added, the pattern becomes "a third copy-paste" unless consolidated. Today's cost of keeping them separate is minor because the pair is only 2.
**Affected files.**
- `ui/src/components/ClaudeSubscriptionPanel.tsx` (1 use — 140 lines)
- `ui/src/components/CodexSubscriptionPanel.tsx` (1 use)
- `ui/src/components/ProviderQuotaCard.tsx` — composes both, dispatches by vendor
**Suggested resolution.** Diff-test the two files. If ≥70% body overlap, collapse to `SubscriptionPanel({ vendor: "claude" | "codex" | … })` with per-vendor window-key config as data. If divergence is higher, keep the pair and let `ProviderQuotaCard` continue to dispatch. Documented as-is in [../patterns/subscription-panel.md](../patterns/subscription-panel.md).
### 4. Sidebar-menu pair: `SidebarAccountMenu` ↔ `SidebarCompanyMenu`
**Problem.** Two dropdown components anchored to the sidebar slot — one for account actions (user profile, sign out), one for company actions (switch company, settings). Same visual affordance (dropdown triggered from a sidebar button), different data subject. Each used twice; neither is a hotspot.
**Affected files.**
- `ui/src/components/SidebarAccountMenu.tsx` (2 uses)
- `ui/src/components/SidebarCompanyMenu.tsx` (2 uses)
**Suggested resolution.** Read both and check the body overlap. Natural consolidations if similar: `<SidebarMenu kind="account" | "company">` driven by kind, or a more general `<SidebarMenu>` that accepts items via a `children` slot. If the bodies are genuinely different (different menu items, different trigger layout), the pair stays and the shared pattern is the sidebar-slot anchoring — which is already captured by `SidebarNavItem`. Documented as-is in [../patterns/sidebar-chrome.md — §The sidebar-menu pair](../patterns/sidebar-chrome.md#the-sidebar-menu-pair).
### 5. Finance card family: `BillerSpendCard` / `FinanceBillerCard` / `FinanceKindCard` / `FinanceTimelineCard` / `AccountingModelCard`
**Problem.** Five cards in the finance/accounting surface, each used 01 times, all sharing the shadcn `Card` family as substrate. Two specific flags surfaced per the 2026-04-21 directive:
1. **`BillerSpendCard``FinanceBillerCard` — likely a true duplicate.** Both name "Biller" in their filename. Both render a per-biller financial summary. They consume **different data models** (`CostByBiller` vs `FinanceByBiller`) — either two genuinely different reporting concepts whose names fail to distinguish them, or one superseded the other and the predecessor survived. Line counts differ (145 vs 44), consistent with a "rich" / "slim" pair.
2. **`AccountingModelCard` is unused.** Zero imports anywhere in the codebase; only appears in Storybook. Either abandoned or awaiting a page.
**Affected files.**
| File | Uses | Data model | Lines |
|---|---|---|---|
| `ui/src/components/BillerSpendCard.tsx` | 1 | `CostByBiller` + `CostByProviderModel` | 145 |
| `ui/src/components/FinanceBillerCard.tsx` | 1 | `FinanceByBiller` | 44 |
| `ui/src/components/FinanceKindCard.tsx` | 1 | `FinanceByKind` (inferred) | — |
| `ui/src/components/FinanceTimelineCard.tsx` | 1 | timeline rollup | — |
| `ui/src/components/AccountingModelCard.tsx` | **0** | — | — |
**Suggested resolution.**
- Diff `BillerSpendCard` vs `FinanceBillerCard` side-by-side. Rename for clarity, merge, or confirm as distinct-but-adjacent.
- Decide `AccountingModelCard`'s fate — adopt it into a page, or delete.
- The broader family (4 cards) is naturally a "Finance / accounting card" pattern. Documented as-is in [../patterns/finance-card.md](../patterns/finance-card.md).
### 6. Row family: `ActivityRow` / `EntityRow` / `IssueRow`
**Problem.** Three row components, one generic and two entity-specific. `EntityRow` is the generic (6 uses), `IssueRow` is issue-specific with an extensive slot interface (3 uses — Inbox + SwipeToArchive), `ActivityRow` is activity-event-specific (2 uses — Activity page). `IssueRow`'s 15-field prop shape (six of them optional `ReactNode` slot props) looks like it could be expressed as `<EntityRow kind="issue" />` plus issue-specific defaults.
**Affected files.**
| File | Uses | Notes |
|---|---|---|
| `ui/src/components/EntityRow.tsx` | 6 | Truly generic — leading / identifier / title / subtitle / trailing slot API |
| `ui/src/components/IssueRow.tsx` | 3 | Composes `StatusIcon`; unread-state + archive action + mobile/desktop split |
| `ui/src/components/ActivityRow.tsx` | 2 | Composes `Identity`, `IssueReferenceActivitySummary` — activity-event-specific |
**Suggested resolution.** Compare `IssueRow` against `EntityRow` directly. If the six slot props plus `unreadState` / `onArchive` can be implemented as `EntityRow` extensions (default slots keyed on `issue`), collapse. `ActivityRow`'s activity-verb formatting is genuinely specialized and is likely worth keeping separate even if `IssueRow` consolidates. Also relevant: the main list pages (Issues, Agents, Projects, …) don't use `EntityRow` today — see [../patterns/list-page.md — Open questions](../patterns/list-page.md#open-questions--risks). Documented as-is in [../patterns/entity-row.md](../patterns/entity-row.md).
### 7. Sidebar triad: `Sidebar` / `InstanceSidebar` / `CompanySettingsSidebar` + `CompanyRail` + `CompanySettingsNav` + `MobileBottomNav`
**Problem.** Six components with overlapping responsibilities — all are "chrome around the main view." Not literal duplicates because each targets a different surface (main nav vs instance settings vs company settings vs company switcher vs tab nav vs mobile bottom bar), but the three-word vocabulary (Sidebar / Rail / Nav) obscures whether these are variants of one structural pattern or separate components that happen to live near each other. They share navigation primitives (`SidebarNavItem`, `SidebarSection`) but not a unifying wrapper. The dead `sidebar-*` tokens in `ui/src/index.css` were designed for this family and none of these consume them (see [tokens-review.md §3](../tokens/tokens-review.md#3-sidebar--tokens-are-dead)).
**Affected files.**
| File | Uses | Role (inferred) |
|---|---|---|
| `ui/src/components/Sidebar.tsx` | ≥3 | Main app navigation |
| `ui/src/components/InstanceSidebar.tsx` | 1 | Instance-settings scope |
| `ui/src/components/CompanySettingsSidebar.tsx` | 2 | Company-settings scope |
| `ui/src/components/CompanyRail.tsx` | 1 (260 lines, dnd-kit-driven) | Sortable company switcher rail |
| `ui/src/components/access/CompanySettingsNav.tsx` | 1 | Settings-page top tab nav |
| `ui/src/components/MobileBottomNav.tsx` | ≥3 | Mobile-bottom-tab alternative |
**Suggested resolution: needs discussion.** Three possible framings:
- **(a) They are genuinely different components** (different layouts, different primitives, different affordances) and the naming convergence is coincidental. Closest to how the code reads today.
- **(b) They are variants of a `<Sidebar variant="main" | "settings" | "rail" | …>`** and should consolidate under one name. Requires auditing their visual shape.
- **(c) They are three distinct patterns** — "sidebar" (persistent rail), "rail" (narrow-strip), "nav" (tab-bar) — and the current spread is correct but the naming convention for picking between them isn't written down anywhere.
Tied to [§Naming inconsistencies — Sidebar / Rail / Nav](#sidebar--rail--nav). Documented as-is in [../patterns/sidebar-chrome.md](../patterns/sidebar-chrome.md).
### 8. Status display triad: `StatusIcon` / `StatusBadge` / `PriorityIcon`
**Problem.** Three components render entity status/priority across the app with the same visual language but different affordances. All three consume `ui/src/lib/status-colors.ts` — a canonical TypeScript catalog mapping status strings to raw Tailwind-palette classes. Two of the three (`StatusIcon`, `PriorityIcon`) type their primary prop as an **untyped string**; `StatusBadge` uses a typed variant. This inconsistency plus the fact that the catalog bypasses the DS token layer makes this a pattern-shape-pending item rather than a straightforward duplicate flag.
**Affected files.**
| File | Uses | Prop type | Notes |
|---|---|---|---|
| `ui/src/components/StatusIcon.tsx` | 14 | `status: string` (untyped) | Circle + popover picker |
| `ui/src/components/StatusBadge.tsx` | 19 | `{ status: string }` wrapping `statusBadge[status]` | 15-line pill |
| `ui/src/components/PriorityIcon.tsx` | 5 | `priority: string` (untyped) | Arrow/triangle + popover picker |
| `ui/src/lib/status-colors.ts` | — | — | Catalog consumed by all three |
| `ui/src/components/AgentActionButtons.tsx` | — | — | Consumes `agentStatusDot` from the same catalog |
**2026-04-21 status.** The token-side of this problem was **partially addressed**: `--signal-success` / `--signal-success-foreground` landed as action-severity tokens paired with `--destructive` (see [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds)). `status-colors.ts` itself is **not** touched — tokenizing its entity-state coloring into a `--status-*` family is a deferred future project. So the two problems here are now independent:
- **Signal (action severity)** = tokens exist, no consumers yet, opt-in.
- **Status (entity state)** = catalog stays as raw Tailwind palette, unchanged.
**Suggested resolution: do not codify a unified status-display pattern in this DS pass.** Pattern shape is explicitly pending the eventual signal-token / status-token scoping. When that project happens, the shape of these three components (typed enum props, class naming, default-fallback behavior) will want to change in sync. Documented as-is in [../patterns/status-display.md](../patterns/status-display.md).
Separately — and independent of the tokens — the `status: string` / `priority: string` untyped props could be typed as string literal unions over the keys of `status-colors.ts`'s records without touching colors. That's a small, self-contained follow-up.
### 9. Quota display: `ProviderQuotaCard` ↔ `QuotaBar`
**Problem.** Two components share the "quota" root name but differ in suffix and in role. `QuotaBar` is a rendering primitive — one horizontal bar with a percent-used fill and a three-level color threshold. `ProviderQuotaCard` is a card composer that *uses* `QuotaBar` (multiple times, for different time windows) plus `ClaudeSubscriptionPanel` / `CodexSubscriptionPanel`. Functionally different, but the `-Bar` / `-Card` naming spread suggests parallelism that isn't there.
Secondary concern: `QuotaBar` hardcodes three-level severity as raw Tailwind palette (`bg-red-400`, `bg-yellow-400`, `bg-green-400`). It is one of four places in the codebase that encode the same red/amber/green severity language without shared tokens — see [../tokens/tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds) and [../patterns/patterns-review.md §6 — Severity indicator](../patterns/patterns-review.md#6-severity-indicator-3-level-health-display--pattern-opportunity).
**Affected files.**
- `ui/src/components/QuotaBar.tsx` (2 uses — 65 lines — primitive bar)
- `ui/src/components/ProviderQuotaCard.tsx` (1 use — 416 lines — card composer that embeds `QuotaBar`)
**Suggested resolution.** Not a consolidation target — different roles. The naming can be clearer if a future refactor happens (e.g., rename the composer to `ProviderQuotaSummary` or `QuotaOverviewCard`). Separate concern: the three-level severity coloring in `QuotaBar` would collapse onto signal/status tokens when that broader work lands. Documented as-is in [../patterns/quota-display.md](../patterns/quota-display.md).
---
## Naming inconsistencies
**Status: deferred (2026-04-21).** All vocabulary decisions below (Dialog-vs-Modal, Picker-vs-Selector, Editor-vs-Form, Sidebar-vs-Rail-vs-Nav, Card-vs-Panel-vs-Widget, file-name casing, prop-vocabulary conventions) are intentionally unresolved. Addressing opportunistically when the relevant code is touched for other reasons, or as a standalone naming-pass project. Each subsection is self-contained — pick any one up cold without reading the others. In pattern docs under `../patterns/`, vocabulary is noted as observed variance (e.g., "dialog pattern — some implementations named `*Modal`, same primitive") rather than as a canonical prescription.
### Container-word proliferation: `Card` vs `Panel` vs `Widget` vs `Modal` vs `Dialog`
Counts of components by suffix:
| Suffix | Count | Examples |
|---|---|---|
| `Card` | 12 | `ApprovalCard`, `BudgetPolicyCard`, `MetricCard`, … |
| `Panel` | 5 | `ActiveAgentsPanel`, `ClaudeSubscriptionPanel`, `PropertiesPanel`, … |
| `Widget` | 1 | `LiveRunWidget` |
| `Modal` | 3 | `DocumentDiffModal`, `ImageGalleryModal`, `PathInstructionsModal` |
| `Dialog` | 6 | `NewAgentDialog`, `ExecutionWorkspaceCloseDialog`, `RoutineRunVariablesDialog`, … |
**Dialog vs Modal:** both Modal- and Dialog-named components use the same `dialog.tsx` primitive. There's no structural distinction — just two names for the same thing. Pick one. The shadcn default is `Dialog`; keeping `Dialog` is the lower-friction move.
**Card vs Panel:** less clear-cut. Rough pattern in this codebase:
- `*Card` when the thing is a discrete piece of content in a grid (`BudgetPolicyCard`, `FinanceBillerCard`).
- `*Panel` when the thing is a larger region that groups related content (`ActiveAgentsPanel`, `PropertiesPanel`).
- But there are violations: `ClaudeSubscriptionPanel` and `AccountingModelCard` look alike structurally and are adjacent in usage.
**Widget (1):** only `LiveRunWidget` uses this. Either absorb into `Card`/`Panel` or codify `Widget` as a distinct concept (e.g., "dashboard-tile with its own data fetch and refresh cadence") and use it consistently.
### `Picker` vs `Selector`
| Name | Uses | Picks what |
|---|---|---|
| `AgentIconPicker` | 13 | An icon |
| `ExecutionParticipantPicker` | 0 (unused) | A participant |
| `ReportsToPicker` | 2 | An agent |
| `InlineEntitySelector` | 1 | An entity |
All four wrap a popover-plus-list. Picker and Selector are synonymous here. Pick one term.
### `Editor` vs `Form`
| Name | Uses | Purpose |
|---|---|---|
| `MarkdownEditor` | 16 | Markdown input |
| `InlineEditor` | 6 | Generic text editor |
| `EnvVarEditor` | 2 | Structured key=value list |
| `RoutineVariablesEditor` | 2 | Structured variable list |
| `ScheduleEditor` | 1 | Cron schedule |
| `AgentConfigForm` | 5 | Full agent config |
| `JsonSchemaForm` | 1 | Schema-driven form |
The line between Editor and Form is fuzzy: `RoutineVariablesEditor` looks like a form, `JsonSchemaForm` could have been named `JsonSchemaEditor`. Suggested rule: **Editor** for content inputs (text, schedule, markdown); **Form** for labeled-field structured forms. Audit whether any renaming is worth the churn; otherwise document the rule and enforce for new additions.
### Sidebar / Rail / Nav
For what are structurally all "chrome around the main content area":
- `Sidebar.tsx`
- `InstanceSidebar.tsx`
- `CompanySettingsSidebar.tsx`
- `CompanyRail.tsx`
- `MobileBottomNav.tsx`
- `access/CompanySettingsNav.tsx`
Pick a default term (`Sidebar`) and use a prefix for variants (`InstanceSidebar`, `CompanySettingsSidebar`). Reserve `Rail` for genuinely different (narrow-strip) affordances, `Nav` for tab-bar-style navigation. The current split is inconsistent with itself.
### File-naming convention is inconsistent
Most composites are `PascalCase.tsx`. Shadcn primitives are `kebab-case.tsx`. But `agent-config-primitives.tsx` and `agent-config-defaults.ts` sit in the top-level composite directory in kebab-case — they don't belong with the shadcn primitives (not in `ui/`) but also don't match the PascalCase of their neighbors.
Suggested fix: rename `agent-config-primitives.tsx``AgentConfigPrimitives.tsx` (or split into per-component files if the 11 exports warrant it) and `agent-config-defaults.ts``agentConfigDefaults.ts` to match JS convention for non-component modules.
### Prop vocabulary is underspecified
A static scan for conventional variant-shaping props (`variant`, `size`, `intent`, `tone`, `kind`, `state`, `status`, `mode`, `level`, `severity`, `priority`) found them used in just **7** components across the codebase (excluding shadcn primitives where they're well-defined):
| Prop | Component | Type |
|---|---|---|
| `variant` | `IssueChatThread` | `"full" \| "embedded"` |
| `variant` | `PageSkeleton` | `... \| "list"` (opaque) |
| `size` | `Identity` | `IdentitySize` |
| `kind` | `ProjectWorkspaceSummaryCard` | `"project_workspace" \| "execution_workspace"` |
| `status` | `StatusIcon` | `string` (**untyped**) |
| `mode` | `RunTranscriptView` | `TranscriptMode` |
| `priority` | `PriorityIcon` | `string` (**untyped**) |
Observations:
- `status: string` in `StatusIcon` and `priority: string` in `PriorityIcon` should be typed enums (matching the keys of `status-colors.ts`).
- `variant` is used for completely unrelated concepts in different components — that's fine semantically, but reinforces that there's no shared prop-vocabulary convention.
- Shadcn primitives (`button`, `badge`) use `variant`/`size` with well-defined CVA enums — these are the model.
---
## Token non-compliance
(Mirror of the Stage 1 token drift findings, but attributed per-component so Stage 3 reviewers can target the worst offenders.)
### Components that hardcode chart/status colors
From [tokens-review.md §1 and §4](../tokens/tokens-review.md):
| Component | Drift |
|---|---|
| `ActivityCharts.tsx` | 17 hardcoded Tailwind-palette hex values for status/priority chart colors; uses `chart-*` tokens zero times. |
| `OrgChart.tsx` (a page) | 6 hardcoded hex values for agent status dot colors. |
| `StatusIcon.tsx` (14 uses) | Consumes `issueStatusIcon` from `status-colors.ts` — raw Tailwind palette classes (`text-blue-600`, `border-violet-600`, etc.). |
| `StatusBadge.tsx` (19 uses) | Consumes `statusBadge` from `status-colors.ts` — raw Tailwind palette classes. |
| `PriorityIcon.tsx` (5 uses) | Consumes `priorityColor` — raw Tailwind palette. |
| `AgentActionButtons.tsx` | Uses `agentStatusDot` — raw Tailwind palette. |
### Components with heavy raw-palette styling
From Stage 1's 659-hit analysis:
- `AgentDetail.tsx` (75 palette hits) — production page
- `RunTranscriptView.tsx` (47 hits) — production component
- `IssueChatThread.tsx` (22 hits) — production component
### Components with arbitrary radius values
See [tokens-review.md §7](../tokens/tokens-review.md#7-arbitrary-radius-values-bypass-the-scale-18-occurrences) for the full list. Production components in the list:
- `CompanyRail.tsx``rounded-[14px]`, `rounded-[22px]`
- Several UxLab pages (acceptable as prototypes)
### Recommendation
Do not extract per-component detail docs for `StatusIcon`, `StatusBadge`, `PriorityIcon`, `AgentActionButtons` as final specs — their color language is blocked on the signal-token decision from [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds). Stage-4 patterns that lean on these (status indicator, priority indicator, agent card with status) will change shape once signal tokens land.
---
## Story coverage gaps
Components with **3+ code uses and no Storybook coverage** — the highest-priority story gaps:
| Component | Uses | Notes |
|---|---|---|
| `StatusIcon` | 14 | Central status-color consumer. Coverage gap tied to §Token non-compliance. |
| `collapsible` (primitive) | 8 | Shadcn primitive. `foundations.stories.tsx` covers many primitives but skips collapsible. |
| `dropdown-menu` (primitive) | 8 | Same. |
| `avatar` (primitive) | 7 | Same — despite Avatar being one of the few components with sub-parts (`AvatarGroup`, `AvatarFallback`, `AvatarBadge`). |
| `skeleton` (primitive) | 6 | Same. |
| `scroll-area` (primitive) | 3 | Same. |
| `ApprovalPayload` | 4 | Feature. |
| `IssueReferencePill` | 4 | Feature. |
| `SidebarNavItem` | 3 | Structural. |
| `RunTranscriptView` | 3 | Feature — the transcript rendering. |
Two categories:
1. **Shadcn primitives missing from `foundations.stories.tsx`.** Small, targeted fix — add them to the existing foundations story.
2. **Production features without a story.** `StatusIcon` is the highest value given its role across 14 call sites.
---
## Unused / low-signal components
### Truly dead (0 imports, no Storybook coverage): 0
None. Every file in `ui/src/components/` either gets imported somewhere or appears in a story.
### Storybook-only (0 imports, appears in a story): 4
These are rendered in a story file but never imported by any page or other component:
| Component | Storybook location (approx.) |
|---|---|
| `AccountingModelCard` | financial/accounting-related story |
| `AgentProperties` | agent-management story |
| `CompanySwitcher` | navigation-layout story |
| `ExecutionParticipantPicker` | (story-referenced; unused in app) |
**Interpretation:** these are either (a) abandoned experiments still living in Storybook, (b) components waiting for the page that uses them, or (c) genuinely unused and should be deleted. Recommend: owner disposition per file. `AgentProperties` is especially surprising given the existence of sibling `IssueProperties`/`GoalProperties`/`ProjectProperties` — possibly a planned-but-not-wired variant of the properties family.
### Below-threshold (12 code uses, no detail file): 76
Not drift — many are legitimately single-use (one-off dialogs, one-off banners). But at 76 out of ~130 it's worth noting: this codebase heavily favors single-use components. A generic-component consolidation pass would likely shrink this group by ~30%. The candidates from §Likely duplicates above are the best places to start.
---
## Plugin SDK hybrid status, prioritization deferred
**Status: RESOLVED as intentional hybrid (2026-04-21).** Not drift. The plugin SDK (`packages/plugins/sdk/src/ui/components.ts`) declares 11 ambient component types; the host implements 2 (`MetricCard`, `StatusBadge`) and leaves the other 9 as contract-only. The 9 unimplemented components now carry a `@status contract-only` JSDoc tag in the SDK source so plugin authors see the status in IDE tooltips at call sites.
Prioritization of which of the 9 to build first is a **separate plugin-SDK roadmap conversation** — not a DS decision and not in scope here. This section captures the current state and the most likely first-implementations when that conversation happens.
### Current state
| SDK contract | Host implementation | Status |
|---|---|---|
| `MetricCard` | [`ui/src/components/MetricCard.tsx`](../../../ui/src/components/MetricCard.tsx) | ✅ implemented |
| `StatusBadge` | [`ui/src/components/StatusBadge.tsx`](../../../ui/src/components/StatusBadge.tsx) | ✅ implemented |
| `DataTable` | — | 🔌 contract-only |
| `TimeseriesChart` | — | 🔌 contract-only |
| `MarkdownBlock` | — | 🔌 contract-only |
| `KeyValueList` | — | 🔌 contract-only |
| `ActionBar` | — | 🔌 contract-only |
| `LogView` | — | 🔌 contract-only |
| `JsonTree` | — | 🔌 contract-only |
| `Spinner` | — | 🔌 contract-only |
| `ErrorBoundary` | — | 🔌 contract-only |
Each of the 9 contract-only entries has a JSDoc block in [`packages/plugins/sdk/src/ui/components.ts`](../../../packages/plugins/sdk/src/ui/components.ts) (lines 253316) that tells plugin authors the runtime will fail and points here.
`PLUGIN_SPEC.md:30` already acknowledged this before the DS extraction: _"The current runtime does not yet ship a real host-provided plugin UI component kit."_
### Implementation notes (for when prioritization happens)
Not prescriptive — candidates surfaced during the 2026-04 extraction:
- **`MarkdownBlock`** — thinnest wrapper around `ui/src/components/MarkdownBody.tsx`. Possibly an alias rather than a new component.
- **`Spinner`** — no matching host component. A ~10-line shadcn-style primitive would be the simplest new build.
- **`KeyValueList`** — patterns exist ad-hoc inside `EntityRow`, `PropertiesPanel` bodies, and `FinanceBillerCard`. Candidate for extraction into a shared primitive.
- **`LogView`** — no counterpart. Transcript rendering is tightly coupled to `RunTranscriptView`; a generic log viewer is a genuine new build.
- **`JsonTree`** — no counterpart. A new build.
- **`ErrorBoundary`** — standard React pattern; a thin wrapper around a React error boundary class.
- **`ActionBar`**, **`DataTable`**, **`TimeseriesChart`** — each is a real component's worth of surface area. Not thin builds.
### Affected files if the prioritization conversation opens
- `packages/plugins/sdk/src/ui/components.ts` (SDK declarations + @status tags)
- `packages/plugins/sdk/src/ui/runtime.ts` (runtime bridge — `renderSdkUiComponent`)
- New host files in `ui/src/components/` for each implemented contract
- [`components/index.md` — Plugin SDK contracts table](./index.md#plugin-sdk-contracts-11) (update implementation column as each lands)

View File

@@ -0,0 +1,228 @@
# Components — Index
- **Generated:** 2026-04-21
- **Repo SHA:** a26e1288b627e82c554445732c7d844648e6b5e1
- **Scope:** `ui/` + plugin SDK contracts
- **Review:** [components-review.md](./components-review.md)
## Counts
- **Total component files:** 135
- **With dedicated detail files (3+ code uses):** 53
- **Below threshold (12 uses):** 76
- **Storybook-only (in stories, 0 code uses):** 4
- **Dead (no uses, no stories):** 0
- **Non-component files (hooks, defaults):** 2
- **Plugin SDK contracts:** 11 (2 implemented by name, 9 contract-only — see §Plugin SDK contracts below)
### By category
| Category | Count |
|---|---|
| composite | 64 |
| primitive | 22 |
| standalone | 47 |
| utility-or-hook | 2 |
**Status markers in the tables below:**
- 📗 **documented** — ≥3 imports, has its own detail file in this directory
- 📘 **below-threshold** — 12 imports, no detail file
- 📙 **storybook-only** — 0 code imports, but appears in a story file
- ☠️ **dead** — 0 imports, 0 stories
- 🔌 **contract-only** — plugin SDK ambient declaration with no matching host implementation
---
## Primitives — `ui/src/components/ui/` (shadcn, 22)
All 22 shadcn primitives, by file name. These are the non-negotiable UI vocabulary — composites should consume these before reaching for custom markup.
| Component | Path | Uses | Pages / Comps | Story |
|---|---|---|---|---|
| 📗 [Button](./Button.md) | `ui/src/components/ui/button.tsx` | 81 | 41 / 38 | ✓ |
| 📗 [Popover](./Popover.md) | `ui/src/components/ui/popover.tsx` | 27 | 6 / 20 | ✓ |
| 📗 [Dialog](./Dialog.md) | `ui/src/components/ui/dialog.tsx` | 21 | 7 / 14 | ✓ |
| 📗 [Input](./Input.md) | `ui/src/components/ui/input.tsx` | 20 | 10 / 10 | ✓ |
| 📗 [Badge](./Badge.md) | `ui/src/components/ui/badge.tsx` | 18 | 11 / 7 | ✓ |
| 📗 [Card](./Card.md) | `ui/src/components/ui/card.tsx` | 18 | 10 / 8 | ✓ |
| 📗 [Tabs](./Tabs.md) | `ui/src/components/ui/tabs.tsx` | 15 | 13 / 2 | ✓ |
| 📗 [Separator](./Separator.md) | `ui/src/components/ui/separator.tsx` | 11 | 7 / 4 | ✓ |
| 📗 [Tooltip](./Tooltip.md) | `ui/src/components/ui/tooltip.tsx` | 11 | 3 / 7 | ✓ |
| 📗 [Select](./Select.md) | `ui/src/components/ui/select.tsx` | 10 | 5 / 5 | ✓ |
| 📗 [Textarea](./Textarea.md) | `ui/src/components/ui/textarea.tsx` | 9 | 4 / 5 | ✓ |
| 📗 [Collapsible](./Collapsible.md) | `ui/src/components/ui/collapsible.tsx` | 8 | 4 / 4 | — |
| 📗 [DropdownMenu](./DropdownMenu.md) | `ui/src/components/ui/dropdown-menu.tsx` | 8 | 3 / 5 | — |
| 📗 [Label](./Label.md) | `ui/src/components/ui/label.tsx` | 8 | 5 / 3 | ✓ |
| 📗 [ToggleSwitch](./ToggleSwitch.md) | `ui/src/components/ui/toggle-switch.tsx` | 8 | 5 / 3 | ✓ |
| 📗 [Avatar](./Avatar.md) | `ui/src/components/ui/avatar.tsx` | 7 | 3 / 4 | — |
| 📗 [Checkbox](./Checkbox.md) | `ui/src/components/ui/checkbox.tsx` | 6 | 4 / 2 | ✓ |
| 📗 [Skeleton](./Skeleton.md) | `ui/src/components/ui/skeleton.tsx` | 6 | 3 / 3 | — |
| 📗 [ScrollArea](./ScrollArea.md) | `ui/src/components/ui/scroll-area.tsx` | 3 | 2 / 1 | — |
| 📘 Breadcrumb | `ui/src/components/ui/breadcrumb.tsx` | 2 | 1 / 1 | — |
| 📘 Command | `ui/src/components/ui/command.tsx` | 2 | 1 / 1 | ✓ |
| 📘 Sheet | `ui/src/components/ui/sheet.tsx` | 2 | 2 / 0 | — |
---
## Composites (64)
Components that import 1+ other component from `@/components/*`. Application-level feature UI.
| Component | Path | Uses | Pages / Comps | Story |
|---|---|---|---|---|
| 📗 [PageSkeleton](./PageSkeleton.md) | `ui/src/components/PageSkeleton.tsx` | 23 | 22 / 1 | ✓ |
| 📗 [EmptyState](./EmptyState.md) | `ui/src/components/EmptyState.tsx` | 20 | 19 / 1 | ✓ |
| 📗 [agent-config-primitives](./agent-config-primitives.md) | `ui/src/components/agent-config-primitives.tsx` | 19 | 4 / 3 | ✓ |
| 📗 [Identity](./Identity.md) | `ui/src/components/Identity.tsx` | 19 | 7 / 12 | ✓ |
| 📗 [StatusIcon](./StatusIcon.md) | `ui/src/components/StatusIcon.tsx` | 14 | 5 / 9 | — |
| 📗 [AgentIconPicker](./AgentIconPicker.md) | `ui/src/components/AgentIconPicker.tsx` | 13 | 4 / 9 | ✓ |
| 📗 [PathInstructionsModal](./PathInstructionsModal.md) | `ui/src/components/PathInstructionsModal.tsx` | 12 | 2 / 3 | ✓ |
| 📗 [PageTabBar](./PageTabBar.md) | `ui/src/components/PageTabBar.tsx` | 10 | 9 / 1 | ✓ |
| 📗 [InlineEntitySelector](./InlineEntitySelector.md) | `ui/src/components/InlineEntitySelector.tsx` | 8 | 2 / 4 | ✓ |
| 📗 [IssuesList](./IssuesList.md) | `ui/src/components/IssuesList.tsx` | 6 | 5 / 1 | ✓ |
| 📗 [AgentConfigForm](./AgentConfigForm.md) | `ui/src/components/AgentConfigForm.tsx` | 5 | 3 / 0 | ✓ |
| 📗 [PriorityIcon](./PriorityIcon.md) | `ui/src/components/PriorityIcon.tsx` | 5 | 2 / 3 | ✓ |
| 📗 [IssueChatThread](./IssueChatThread.md) | `ui/src/components/IssueChatThread.tsx` | 4 | 2 / 2 | ✓ |
| 📗 [ApprovalCard](./ApprovalCard.md) | `ui/src/components/ApprovalCard.tsx` | 3 | 2 / 1 | ✓ |
| 📗 [BudgetPolicyCard](./BudgetPolicyCard.md) | `ui/src/components/BudgetPolicyCard.tsx` | 3 | 3 / 0 | ✓ |
| 📗 [IssueFiltersPopover](./IssueFiltersPopover.md) | `ui/src/components/IssueFiltersPopover.tsx` | 3 | 1 / 2 | ✓ |
| 📗 [IssueLinkQuicklook](./IssueLinkQuicklook.md) | `ui/src/components/IssueLinkQuicklook.tsx` | 3 | 0 / 2 | ✓ |
| 📗 [IssueWorkspaceCard](./IssueWorkspaceCard.md) | `ui/src/components/IssueWorkspaceCard.tsx` | 3 | 1 / 2 | ✓ |
| 📗 [RoutineRunVariablesDialog](./RoutineRunVariablesDialog.md) | `ui/src/components/RoutineRunVariablesDialog.tsx` | 3 | 2 / 1 | ✓ |
| 📗 [WorkspaceRuntimeControls](./WorkspaceRuntimeControls.md) | `ui/src/components/WorkspaceRuntimeControls.tsx` | 3 | 2 / 1 | ✓ |
| 📘 AgentActionButtons | `ui/src/components/AgentActionButtons.tsx` | 2 | 2 / 0 | ✓ |
| 📘 CommandPalette | `ui/src/components/CommandPalette.tsx` | 2 | 0 / 2 | ✓ |
| 📘 IssueColumns | `ui/src/components/IssueColumns.tsx` | 2 | 1 / 1 | ✓ |
| 📘 IssueContinuationHandoff | `ui/src/components/IssueContinuationHandoff.tsx` | 2 | 1 / 1 | ✓ |
| 📘 IssueDocumentsSection | `ui/src/components/IssueDocumentsSection.tsx` | 2 | 1 / 1 | ✓ |
| 📘 IssueProperties | `ui/src/components/IssueProperties.tsx` | 2 | 1 / 1 | ✓ |
| 📘 NewIssueDialog | `ui/src/components/NewIssueDialog.tsx` | 2 | 0 / 2 | ✓ |
| 📘 OutputFeedbackButtons | `ui/src/components/OutputFeedbackButtons.tsx` | 2 | 0 / 2 | — |
| 📘 ProjectWorkspaceSummaryCard | `ui/src/components/ProjectWorkspaceSummaryCard.tsx` | 2 | 0 / 2 | ✓ |
| 📘 ReportsToPicker | `ui/src/components/ReportsToPicker.tsx` | 2 | 1 / 1 | ✓ |
| 📘 RoutineVariablesEditor | `ui/src/components/RoutineVariablesEditor.tsx` | 2 | 2 / 0 | ✓ |
| 📘 Sidebar | `ui/src/components/Sidebar.tsx` | 2 | 0 / 2 | ✓ |
| 📘 SidebarAccountMenu | `ui/src/components/SidebarAccountMenu.tsx` | 2 | 0 / 2 | ✓ |
| 📘 SidebarCompanyMenu | `ui/src/components/SidebarCompanyMenu.tsx` | 2 | 0 / 2 | ✓ |
| 📘 CompanySettingsNav | `ui/src/components/access/CompanySettingsNav.tsx` | 1 | 0 / 1 | — |
| 📘 ModeBadge | `ui/src/components/access/ModeBadge.tsx` | 1 | 1 / 0 | — |
| 📘 BillerSpendCard | `ui/src/components/BillerSpendCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 BreadcrumbBar | `ui/src/components/BreadcrumbBar.tsx` | 1 | 0 / 1 | ✓ |
| 📘 BudgetIncidentCard | `ui/src/components/BudgetIncidentCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 CommentThread | `ui/src/components/CommentThread.tsx` | 1 | 0 / 1 | ✓ |
| 📘 CompanyRail | `ui/src/components/CompanyRail.tsx` | 1 | 0 / 1 | ✓ |
| 📘 DocumentDiffModal | `ui/src/components/DocumentDiffModal.tsx` | 1 | 0 / 1 | ✓ |
| 📘 FilterBar | `ui/src/components/FilterBar.tsx` | 1 | 1 / 0 | ✓ |
| 📘 FinanceBillerCard | `ui/src/components/FinanceBillerCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 FinanceKindCard | `ui/src/components/FinanceKindCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 FinanceTimelineCard | `ui/src/components/FinanceTimelineCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 GoalProperties | `ui/src/components/GoalProperties.tsx` | 1 | 1 / 0 | ✓ |
| 📘 IssuesQuicklook | `ui/src/components/IssuesQuicklook.tsx` | 1 | 0 / 1 | ✓ |
| 📘 JsonSchemaForm | `ui/src/components/JsonSchemaForm.tsx` | 1 | 1 / 0 | ✓ |
| 📘 KeyboardShortcutsCheatsheet | `ui/src/components/KeyboardShortcutsCheatsheet.tsx` | 1 | 0 / 1 | ✓ |
| 📘 NewAgentDialog | `ui/src/components/NewAgentDialog.tsx` | 1 | 0 / 1 | ✓ |
| 📘 NewGoalDialog | `ui/src/components/NewGoalDialog.tsx` | 1 | 0 / 1 | ✓ |
| 📘 NewProjectDialog | `ui/src/components/NewProjectDialog.tsx` | 1 | 0 / 1 | ✓ |
| 📘 OnboardingWizard | `ui/src/components/OnboardingWizard.tsx` | 1 | 0 / 0 | ✓ |
| 📘 ProjectProperties | `ui/src/components/ProjectProperties.tsx` | 1 | 1 / 0 | ✓ |
| 📘 PropertiesPanel | `ui/src/components/PropertiesPanel.tsx` | 1 | 0 / 1 | — |
| 📘 ProviderQuotaCard | `ui/src/components/ProviderQuotaCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 ScheduleEditor | `ui/src/components/ScheduleEditor.tsx` | 1 | 1 / 0 | ✓ |
| 📘 SidebarAgents | `ui/src/components/SidebarAgents.tsx` | 1 | 0 / 1 | — |
| 📘 SidebarProjects | `ui/src/components/SidebarProjects.tsx` | 1 | 0 / 1 | — |
| 📙 AccountingModelCard | `ui/src/components/AccountingModelCard.tsx` | 0 | 0 / 0 | ✓ |
| 📙 AgentProperties | `ui/src/components/AgentProperties.tsx` | 0 | 0 / 0 | ✓ |
| 📙 CompanySwitcher | `ui/src/components/CompanySwitcher.tsx` | 0 | 0 / 0 | ✓ |
| 📙 ExecutionParticipantPicker | `ui/src/components/ExecutionParticipantPicker.tsx` | 0 | 0 / 0 | ✓ |
---
## Standalones (47)
Components that import no other `@/components/*`. Usually: icons, self-contained widgets, components that only depend on radix / lucide / local libs. The fact that they import zero composites or primitives is itself a data point — some of these probably should be using primitives.
| Component | Path | Uses | Pages / Comps | Story |
|---|---|---|---|---|
| 📗 [StatusBadge](./StatusBadge.md) | `ui/src/components/StatusBadge.tsx` | 19 | 12 / 7 | ✓ |
| 📗 [MarkdownEditor](./MarkdownEditor.md) | `ui/src/components/MarkdownEditor.tsx` | 16 | 5 / 9 | ✓ |
| 📗 [MarkdownBody](./MarkdownBody.md) | `ui/src/components/MarkdownBody.tsx` | 11 | 5 / 6 | ✓ |
| 📗 [EntityRow](./EntityRow.md) | `ui/src/components/EntityRow.tsx` | 6 | 6 / 0 | ✓ |
| 📗 [InlineEditor](./InlineEditor.md) | `ui/src/components/InlineEditor.tsx` | 6 | 4 / 2 | ✓ |
| 📗 [ApprovalPayload](./ApprovalPayload.md) | `ui/src/components/ApprovalPayload.tsx` | 4 | 2 / 2 | — |
| 📗 [CompanyPatternIcon](./CompanyPatternIcon.md) | `ui/src/components/CompanyPatternIcon.tsx` | 4 | 3 / 1 | ✓ |
| 📗 [IssueReferencePill](./IssueReferencePill.md) | `ui/src/components/IssueReferencePill.tsx` | 4 | 1 / 3 | — |
| 📗 [ActivityCharts](./ActivityCharts.md) | `ui/src/components/ActivityCharts.tsx` | 3 | 2 / 1 | ✓ |
| 📗 [CopyText](./CopyText.md) | `ui/src/components/CopyText.tsx` | 3 | 2 / 1 | ✓ |
| 📗 [IssueRow](./IssueRow.md) | `ui/src/components/IssueRow.tsx` | 3 | 1 / 2 | ✓ |
| 📗 [PackageFileTree](./PackageFileTree.md) | `ui/src/components/PackageFileTree.tsx` | 3 | 3 / 0 | ✓ |
| 📗 [SidebarNavItem](./SidebarNavItem.md) | `ui/src/components/SidebarNavItem.tsx` | 3 | 0 / 3 | — |
| 📗 [RunTranscriptView](./RunTranscriptView.md) | `ui/src/components/transcript/RunTranscriptView.tsx` | 3 | 2 / 1 | — |
| 📘 ActivityRow | `ui/src/components/ActivityRow.tsx` | 2 | 2 / 0 | ✓ |
| 📘 AsciiArtAnimation | `ui/src/components/AsciiArtAnimation.tsx` | 2 | 1 / 1 | ✓ |
| 📘 BudgetSidebarMarker | `ui/src/components/BudgetSidebarMarker.tsx` | 2 | 0 / 2 | ✓ |
| 📘 CloudAccessGate | `ui/src/components/CloudAccessGate.tsx` | 2 | 0 / 0 | — |
| 📘 CompanySettingsSidebar | `ui/src/components/CompanySettingsSidebar.tsx` | 2 | 0 / 2 | — |
| 📘 EnvVarEditor | `ui/src/components/EnvVarEditor.tsx` | 2 | 0 / 2 | ✓ |
| 📘 ExecutionWorkspaceCloseDialog | `ui/src/components/ExecutionWorkspaceCloseDialog.tsx` | 2 | 1 / 1 | ✓ |
| 📘 GoalTree | `ui/src/components/GoalTree.tsx` | 2 | 2 / 0 | ✓ |
| 📘 IssueGroupHeader | `ui/src/components/IssueGroupHeader.tsx` | 2 | 1 / 1 | ✓ |
| 📘 IssueReferenceActivitySummary | `ui/src/components/IssueReferenceActivitySummary.tsx` | 2 | 1 / 1 | — |
| 📘 IssueRelatedWorkPanel | `ui/src/components/IssueRelatedWorkPanel.tsx` | 2 | 1 / 1 | — |
| 📘 IssueRunLedger | `ui/src/components/IssueRunLedger.tsx` | 2 | 1 / 1 | ✓ |
| 📘 Layout | `ui/src/components/Layout.tsx` | 2 | 0 / 1 | — |
| 📘 MetricCard | `ui/src/components/MetricCard.tsx` | 2 | 2 / 0 | ✓ |
| 📘 OpenCodeLogoIcon | `ui/src/components/OpenCodeLogoIcon.tsx` | 2 | 0 / 1 | — |
| 📘 ProjectWorkspacesContent | `ui/src/components/ProjectWorkspacesContent.tsx` | 2 | 2 / 0 | ✓ |
| 📘 QuotaBar | `ui/src/components/QuotaBar.tsx` | 2 | 0 / 2 | ✓ |
| 📘 RunChatSurface | `ui/src/components/RunChatSurface.tsx` | 2 | 0 / 2 | ✓ |
| 📘 ScrollToBottom | `ui/src/components/ScrollToBottom.tsx` | 2 | 2 / 0 | — |
| 📘 SwipeToArchive | `ui/src/components/SwipeToArchive.tsx` | 2 | 1 / 1 | ✓ |
| 📘 ActiveAgentsPanel | `ui/src/components/ActiveAgentsPanel.tsx` | 1 | 1 / 0 | ✓ |
| 📘 ClaudeSubscriptionPanel | `ui/src/components/ClaudeSubscriptionPanel.tsx` | 1 | 0 / 1 | ✓ |
| 📘 CodexSubscriptionPanel | `ui/src/components/CodexSubscriptionPanel.tsx` | 1 | 0 / 1 | ✓ |
| 📘 DevRestartBanner | `ui/src/components/DevRestartBanner.tsx` | 1 | 0 / 1 | — |
| 📘 HermesIcon | `ui/src/components/HermesIcon.tsx` | 1 | 0 / 0 | — |
| 📘 ImageGalleryModal | `ui/src/components/ImageGalleryModal.tsx` | 1 | 1 / 0 | ✓ |
| 📘 InstanceSidebar | `ui/src/components/InstanceSidebar.tsx` | 1 | 0 / 1 | — |
| 📘 KanbanBoard | `ui/src/components/KanbanBoard.tsx` | 1 | 0 / 1 | ✓ |
| 📘 LiveRunWidget | `ui/src/components/LiveRunWidget.tsx` | 1 | 1 / 0 | ✓ |
| 📘 MobileBottomNav | `ui/src/components/MobileBottomNav.tsx` | 1 | 0 / 1 | ✓ |
| 📘 SidebarSection | `ui/src/components/SidebarSection.tsx` | 1 | 0 / 1 | — |
| 📘 ToastViewport | `ui/src/components/ToastViewport.tsx` | 1 | 0 / 1 | — |
| 📘 WorktreeBanner | `ui/src/components/WorktreeBanner.tsx` | 1 | 0 / 1 | ✓ |
---
## Non-component files (2)
These live in `ui/src/components/` by convention but don't export React components.
| File | Path | Role |
|---|---|---|
| `agent-config-defaults` | `ui/src/components/agent-config-defaults.ts` | module with shared constants/defaults |
| `useLiveRunTranscripts` | `ui/src/components/transcript/useLiveRunTranscripts.ts` | React hook |
---
## Plugin SDK contracts (11)
Ambient component declarations from [`packages/plugins/sdk/src/ui/components.ts`](../../../packages/plugins/sdk/src/ui/components.ts). These are types-only; the host provides implementations at runtime via `renderSdkUiComponent(name, props)`.
> **Hybrid status is intentional (2026-04-21 decision).** Two components are implemented by the host. The other nine are **contract-only** — the types exist so plugin authors can code against them, but rendering today will fail at runtime. The 9 contract-only components carry a `@status contract-only` JSDoc tag in the SDK source, which appears in IDE tooltips at call sites. Prioritization of which to implement first is a separate plugin-SDK roadmap conversation, not a DS decision. See [components-review.md §Plugin SDK hybrid status](./components-review.md#plugin-sdk-hybrid-status-prioritization-deferred).
| SDK Component | Implementation | Status |
|---|---|---|
| `MetricCard` | [`ui/src/components/MetricCard.tsx`](../../../ui/src/components/MetricCard.tsx) | 📗 **implemented** |
| `StatusBadge` | [`ui/src/components/StatusBadge.tsx`](../../../ui/src/components/StatusBadge.tsx) | 📗 **implemented** |
| `DataTable` | — | 🔌 **contract-only** |
| `TimeseriesChart` | — | 🔌 **contract-only** (distinct from `ActivityCharts.tsx`, which has a different API) |
| `MarkdownBlock` | — | 🔌 **contract-only** (`MarkdownBody.tsx` is the host's markdown renderer, name differs) |
| `KeyValueList` | — | 🔌 **contract-only** |
| `ActionBar` | — | 🔌 **contract-only** (`AgentActionButtons.tsx` is role-specific, not a match) |
| `LogView` | — | 🔌 **contract-only** |
| `JsonTree` | — | 🔌 **contract-only** |
| `Spinner` | — | 🔌 **contract-only** |
| `ErrorBoundary` | — | 🔌 **contract-only** |
All 9 contract-only entries carry `@status contract-only` in their JSDoc block (see [`packages/plugins/sdk/src/ui/components.ts`](../../../packages/plugins/sdk/src/ui/components.ts) lines 253316).

View File

@@ -0,0 +1,69 @@
# Detail Page
Full-page layout for viewing and editing a single entity (agent, issue, project, goal, routine, approval, or execution/project workspace).
**Instances: 8.** `AgentDetail`, `IssueDetail`, `ProjectDetail`, `GoalDetail`, `RoutineDetail`, `ApprovalDetail`, `ExecutionWorkspaceDetail`, `ProjectWorkspaceDetail`.
## Composition (shared baseline)
Measured across the 8 instances by import intersection:
- **`button`** — 8/8 (every detail page has actions in its header)
- **`tabs`** — 6/8 (detail pages split sub-views by tab)
- **`PageSkeleton`** — 5/8 (loading state while the entity is being fetched)
- **`StatusBadge`** — 4/8 (status is surfaced in the header area)
- **`separator`** — 4/8
- Breadcrumb context (`useBreadcrumbs`) — all 8 set a breadcrumb trail for the entity.
[INFER] Structural template, from reading AgentDetail and IssueDetail:
```
<PageSkeleton or <ContentLoaded>
<Breadcrumb / back-nav>
<Header>
<Title> — entity name + identifier
<StatusBadge> — where applicable (issue, run, approval, agent)
<Actions> — edit, archive, more-menu
</Header>
<Tabs> — 25 tabs (overview, config, activity, …)
<TabContent>
… entity-specific body (properties, related work, charts, transcripts)
</TabContent>
</Tabs>
</>
```
## Canonical instance
`ui/src/pages/IssueDetail.tsx` is the most mature and most-cross-referenced implementation. `ui/src/pages/AgentDetail.tsx` is second and shows the tab-bar with many sub-surfaces.
## Variance across instances
Observed differences that may be intentional (different entity domain) or may be drift:
| Instance | Breadcrumbs | Tabs | Status element | Loading state | Notes |
|---|---|---|---|---|---|
| `IssueDetail` | yes | yes | `StatusIcon` + `StatusBadge` | custom | largest file; widely-referenced |
| `AgentDetail` | yes | yes | `StatusBadge` + `agentStatusDot` | `PageSkeleton` | composes `AgentConfigForm`, `ActivityCharts`, `PackageFileTree`, `RunTranscriptView` |
| `ProjectDetail` | yes | yes | `StatusBadge` | `PageSkeleton` | |
| `GoalDetail` | yes | (unconfirmed) | `StatusBadge` | `PageSkeleton` | |
| `RoutineDetail` | yes | yes | (unconfirmed) | `PageSkeleton` | |
| `ApprovalDetail` | yes | (none) | tone-coded via `ApprovalCard` | `PageSkeleton` | diverges — simpler shape |
| `ExecutionWorkspaceDetail` | yes | — | — | — | newer; diverges |
| `ProjectWorkspaceDetail` | yes | — | — | — | newer; diverges |
- **3 of 8 don't use `PageSkeleton`** — worth confirming each has an equivalent loading state.
- **`ApprovalDetail` skips tabs** — likely correct for its single-surface nature.
- **Status element is inconsistent** — `StatusBadge` alone, `StatusIcon + StatusBadge`, `StatusBadge + agentStatusDot` (separate dot helper), `ApprovalCard` tone encoding. Tied to [status-display.md](./status-display.md) and [../tokens/tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds).
## Related components and patterns
- Loading state: [`PageSkeleton`](../components/index.md) (documented as a component — used in 22+ places)
- Empty state: [`EmptyState`](../components/index.md) (mostly used on list pages)
- Status badge: [`StatusBadge`](../components/StatusBadge.md), [`StatusIcon`](../components/index.md) — see [status-display.md](./status-display.md)
- Tab bar: shadcn `tabs`, and a custom [`PageTabBar`](../components/PageTabBar.md) used on some pages
## Open questions / risks
- Whether to codify a `<DetailPageHeader>` composite (title + status + actions block) to reduce per-page drift. Four detail pages already diverge on which status element they use.
- The new `*WorkspaceDetail` pages do not yet share much structure with the older four. Check before Stage-4 pattern extraction runs again in a future quarter.

View File

@@ -0,0 +1,51 @@
# Entity-Creation Dialog
Dialog surface for creating a new entity (agent, goal, issue, project).
**Instances: 4.** `NewAgentDialog`, `NewGoalDialog`, `NewIssueDialog`, `NewProjectDialog`.
> **Extraction-only pass.** This pattern document records the family as it exists today. It does not prescribe a merge into a single generic `NewEntityDialog`. See [components-review.md §Likely duplicates #1](../components/components-review.md#1-entity-creation-dialog-family-newagentdialog--newgoaldialog--newissuedialog--newprojectdialog) for the open-question treatment.
## Instances
| File | Lines | Uses | Opened via |
|---|---|---|---|
| `ui/src/components/NewAgentDialog.tsx` | 210 | 1 | `useDialog().newAgentOpen` |
| `ui/src/components/NewGoalDialog.tsx` | (unread) | 1 | `useDialog()` |
| `ui/src/components/NewIssueDialog.tsx` | 1699 | 2 | `useDialog()` |
| `ui/src/components/NewProjectDialog.tsx` | (unread) | 1 | `useDialog()` |
## Composition (shared)
All four:
- Import `Dialog` + `DialogContent` from `@/components/ui/dialog` (primitive).
- Consume a central `useDialog()` context from `ui/src/context/DialogContext` that exposes open/close flags per entity type.
- Call an entity-specific API on submit (`agentsApi.create`, `issuesApi.create`, …) via `useMutation`.
- Dismiss via `closeNewX()` from the same context.
## Shape divergence
The instances are **not structurally equivalent.** Line counts alone:
- `NewAgentDialog` = 210 lines (adapter picker → create stub)
- `NewIssueDialog` = 1699 lines (rich form: assignees, projects, policies, mentions, dragdrop, advanced panel)
Other divergence indicators from imports:
- `NewIssueDialog` imports `agent-config-primitives` (`DraftInput`, `ChoosePathButton`, etc.), `ToggleSwitch`, `Popover`, large piece of `@dnd-kit`, markdown editors — i.e. a full inline form.
- `NewAgentDialog` imports `Dialog`, `Button`, adapter-registry helpers — a chooser, not a full form.
- `NewGoalDialog` and `NewProjectDialog` not examined in detail here; their size is likely between the two extremes.
## Open questions / risks
- Is `NewIssueDialog` intended to be "the" form and the others are just chooser-stubs that redirect to a detail page? That shape would be load-bearing. Currently unclear from static reading.
- Without a generic base, adding a fifth entity (e.g. `NewRoutineDialog`) means another copy of the dialog-open-context wiring. The `useDialog()` context already carries the per-entity open/close flags — it would be the natural integration point if consolidation is pursued.
## Pattern use in Stage 4 analysis
If Stage 4 were to name a composition "new-entity-dialog" for future reference, the canonical definition would be:
> A `<Dialog>` opened from the `useDialog()` context, closed via a per-entity handler, containing an entity-specific body that submits through the matching API and invalidates the matching `queryKeys` on success.
Not yet codified as code. Documented here only.

View File

@@ -0,0 +1,62 @@
# Entity Properties Panel
Side-panel content that shows an entity's metadata, lets the user edit inline, and drives per-field save state.
**Instances: 4 entity-specific panels + 1 generic panel.**
`AgentProperties`, `GoalProperties`, `IssueProperties`, `ProjectProperties` + `PropertiesPanel`.
> **Extraction-only pass.** This pattern does not prescribe a merge. The open question about whether `PropertiesPanel` composes or duplicates the four entity-specific panels is surfaced — not resolved. See [components-review.md §Likely duplicates #2](../components/components-review.md#2-properties-panel-family-agentproperties--goalproperties--issueproperties--projectproperties--generic-propertiespanel).
## Instances
| Component | Lines | Uses | Role |
|---|---|---|---|
| `PropertiesPanel.tsx` | 29 | 1 | **Generic chrome.** A slide-in `<aside>` that reads `panelContent` from `usePanel()` context and renders whatever the caller has set. |
| `AgentProperties.tsx` | (unread) | **0** | Entity-specific body (currently unused in production — Storybook-only). |
| `GoalProperties.tsx` | (unread) | 1 | Entity-specific body. |
| `IssueProperties.tsx` | 1370 | 2 | Entity-specific body; imports `StatusIcon`, `PriorityIcon`, `Identity`, `IssueReferencePill`, plus form primitives. |
| `ProjectProperties.tsx` | 1140 | 1 | Entity-specific body; imports `StatusBadge`, status-colors consumers, `InlineEditor`, `EnvVarEditor`. |
## Relationship: chrome vs content
Reading `PropertiesPanel.tsx` (29 lines):
```tsx
export function PropertiesPanel() {
const { panelContent, panelVisible, setPanelVisible } = usePanel();
if (!panelContent) return null;
return (
<aside className="… bg-card …">
<div className="… flex flex-col …">
<Header><Button icon="X" onClick={close} /></Header>
<ScrollArea><div className="p-4">{panelContent}</div></ScrollArea>
</div>
</aside>
);
}
```
So `PropertiesPanel` is a **slot**, not a content template. The four `*Properties` components are **contents** that get passed into that slot via `usePanel().setPanelContent(...)`.
## Open question (not resolved here)
Does `PropertiesPanel` already compose the four entity-specific panels, or do the four duplicate work that `PropertiesPanel` could own?
Evidence either way from a static read:
- **For "composes"**: the four entity-specific panels don't render a dialog/drawer wrapper themselves — each emits just the body content. They rely on some parent (either `PropertiesPanel` or a page-owned slot) to provide the outer container.
- **For "duplicates"**: the header layout (`Properties` title + close button) is in `PropertiesPanel`, but each of the four is 1100+ lines of its own scaffolding (section headers, separators, form fields, save-state handling) that *could* be factored into a `<PropertiesPanelBody>` helper.
**Not a call to make in this extraction.** The founder should open `IssueProperties.tsx` and `ProjectProperties.tsx` side-by-side and judge.
## Also noted
- `AgentProperties.tsx` has **0 production uses** (Storybook-only). Either abandoned or waiting for a page. See [components-review.md §Unused components](../components/components-review.md#unused--low-signal-components).
- Each entity-specific panel consumes `status-colors.ts` for status-color rendering (directly or via `StatusBadge`/`StatusIcon`), inheriting the token drift from [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds).
- File sizes (11001370 lines each) suggest each panel handles its own save pipeline, field-level error states, mutations, and recent-selection tracking (indirectly observed via imports from `recent-assignees`, `recent-projects`, etc.). Whether that logic is shareable is the real design question underneath the styling question.
## Related components and patterns
- Chrome slot: `PropertiesPanel` and the [`usePanel()` context](../../../ui/src/context/PanelContext.tsx)
- Status coloring: [status-display.md](./status-display.md)
- Inline field editing primitives in [`agent-config-primitives.tsx`](../components/agent-config-primitives.md) (`DraftInput`, `InlineField`, `ToggleField`, `ToggleWithNumber`)

Some files were not shown because too many files have changed in this diff Show More