Commit Graph

3668 Commits

Author SHA1 Message Date
Tonio e07d605dfc
test(ui): wait for conditions, not durations, in three flaky tests (#11499)
Three tests yielded a fixed number of macrotasks before asserting - five in one
case, one in another - which is ample on an idle machine and not when the suite
runs many workers in parallel. The container was still empty, or the state had
not landed, and the assertion failed on behaviour that works. `vi.waitFor`
retries against a time budget instead, so a loaded worker gets more turns
rather than a failure.

`DocumentAnnotationPopover` is a different race and is fixed differently. The
popover element is in the DOM as soon as React commits, while the effect that
registers the document-level keydown and pointerdown listeners runs afterwards.
A test dispatching in that gap loses the event outright, and a lost event
cannot be recovered by retrying an assertion - so the render is wrapped in
`act` to flush passive effects, and the waits only cover the smaller race that
remains.

Refs #11484.

Verified stable over six consecutive runs of the three files, and the full ui
suite passes. Other instances of the same class remain: the full suite still
shows an occasional failure in a different unrelated test on each run.
`App.cases-routing.test.tsx:104-108` is the clearest one - the identical
fixed-turn loop this PR replaced in its sibling `App.activity-routing.test.tsx`,
three turns instead of five - and takes the same one-line fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:29:42 -07:00
Tonio 870c305410
refactor(ui): drop the TZ pin now the fixtures are anchored (#11508)
#11480 pinned `TZ: "UTC"` in the vitest config because several suites asserted
local-time renders from UTC instants. That made the suite green everywhere, but
by suppressing the variable rather than fixing what depended on it: afterwards
no test could observe non-UTC behaviour, and a fixture quietly regaining a
local-time dependency would not be caught.

#11478 anchored those fixtures to the clock under test, which is the real fix,
so the pin now carries only its cost. Removed.

The order was load-bearing and is now satisfied. Measured on master before
#11478 landed, removing the pin failed four date-dependent tests at UTC+9 and
ten at UTC+12 - IssueProperties, IssueThreadInteractionCard, SummarySlotCard
and attention, all of which that PR anchors. Re-measured on master at
40e7add71 with the pin removed: 4117 pass at UTC+14, UTC+9 and UTC-11, against
a control of 4117 with the pin. The prerequisite is demonstrated rather than
assumed.

No test accompanies this, and the prefix says so. The change deletes
configuration, and what verifies it is the existing suite run at several
offsets - not expressible as a test case without a harness that re-runs vitest
under a different TZ.

A caution for anyone reading a failure here later: an earlier pass at this
misread the parallel-worker flakes from #11499 as timezone failures, because a
single run per zone showed a clean east-of-UTC pattern that was really noise.
Date-dependent failures are consistent across runs and name date-handling
tests; the flakes vary between runs and name unrelated pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:18:13 -07:00
Tonio 6d0adbfb5d
refactor(ui): retire the shared-cache reasoning the account key made obsolete (#11507)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Three places in the UI turn the company list into an authorization
verdict: the invite landing page, the onboarding draft gate, and company
auto-selection
> - Each grew a defense when one `["companies"]` cache entry answered
for every account, and each documented that hazard at length
> - #11488 keyed the entry by account, so the hazard those comments
describe can no longer happen
> - The comments stayed, and a comment that describes a trap that no
longer exists is how the next reader removes a mechanism that is still
holding something up
> - This pull request replaces that reasoning with what the mechanisms
actually do now, and removes the one condition that genuinely went dead
> - The benefit is that the next person to simplify these gates has
accurate reasons to work from

## Linked Issues or Issue Description

No public issue exists. Follow-up to #11488, #11430 and #11417. The
problem follows.

**What happened?**

The three gates were written against a shared, account-less company
cache. #11488 keyed that entry by account, which made the documented
hazard impossible — but the documentation stayed. Each gate now carries
a long explanation of a cross-account leak that the key prevents, while
the mechanism it explains is in fact still required for a different and
unrelated reason.

That is a maintenance hazard in a specific direction: a reader who
checks the comment against the code concludes the mechanism is obsolete,
removes it, and reintroduces a failure the comment never mentioned.

**Expected behavior**

The reasoning next to each gate describes why the gate is there now.

**Steps to reproduce**

Read the comment above `ownershipDecidable` in `OnboardingWizard.tsx`
against `master`. It justifies `isSuccess` on the grounds that "after an
account switch the retained value is the previous account's list", which
the account-keyed entry makes impossible.

**Paperclip version or commit**

`master` at `0817fbad9`.

## What Changed

- `ui/src/pages/InviteLanding.tsx` — dropped the
`Boolean(sessionQuery.data)` conjunct from `membershipListIsCurrent`;
rewrote the comment.
- `ui/src/components/OnboardingWizard.tsx` — replaced the shared-cache
explanation above the ownership gate with the reason the gate still
exists.
- `ui/src/hooks/useSignOut.ts` — corrected the sweep's rationale, which
cited the company list as its example of data the next account could
read.

### The one dead condition

`membershipListIsCurrent` tested `Boolean(sessionQuery.data) &&
companiesQuery.isFetchedAfterMount`. The first term cannot be false when
the second is true: the query is `enabled` only while a session exists,
so the flag cannot be set without one. The lapsed-session case it looked
like it covered is covered by the keying instead — the observer re-keys
to the anonymous entry and holds no data to leak.

Tests pass with it removed, but that only shows no test distinguishes
it, which is why the reasoning above is recorded in the code rather than
left for the next reader to redo.

### What is deliberately kept

Each gate turned out to be load-bearing for a reason that has nothing to
do with accounts:

- **InviteLanding** still waits for a list fetched this mount. A pending
query reads as an empty list, which reads as "not a member", which
auto-accepts an invite the customer may already hold.
- **OnboardingWizard** still forces a fetch with `staleTime: 0`. A
cached list is the right account's but can be thirty seconds old, so a
company created moments ago in another tab is missing from it — and
missing reads as "you do not own this", which *deletes* the draft rather
than withholding it.
- **CompanyProvider** still clears the live selection on an account
change. That is component state and does not change key with the query.

Removing them as redundant is the mistake the stale comments invited;
this change is what makes that argument harder to make by accident.

## Verification

- `pnpm tsc -b` in `ui`: clean.
- `InviteLanding.test.tsx`, `OnboardingWizard.test.tsx`,
`CompanyContext.test.tsx`, `useSignOut.test.tsx`,
`companies-query.test.ts`: **67 passed**, run twice.

No behaviour change is claimed and none is intended: the only
non-comment edit is the removal of a condition that cannot alter the
expression's value.

**Not done:** no browser run. Nothing here is observable at runtime.

## Risks

Low. Comments, plus one condition shown to be unreachable-false.

The risk that remains is a documentation risk in the other direction: if
the keying is ever reverted or bypassed, these comments will understate
what the gates protect against. They name #11488 so that connection is
findable.

**This does not close the class.** Account-scoped entries other than the
list — `["companies", id]`, stats, and the rest — still survive an
account change that skips the sign-out button. That is unclaimed work,
and larger than this.

## Model Used

Claude Opus 5 (`claude-opus-5`), through Claude Code. Extended thinking
enabled. Tool use enabled: file read and edit, shell for typecheck and
test runs.

## Checklist

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

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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:34:55 -07:00
Tonio 40e7add71c
test(ui): make the suite independent of the machine timezone (#11478)
Several suites asserted local-time renders from UTC instants, or built date
fixtures from the real clock, so they only held where local time happened to
match. CI runs in UTC and never reported it; a contributor anywhere else saw
failures on a clean checkout.

Seven files, anchored to the clock under test rather than to the machine's.
The set grew twice while being fixed: the two tests the issue named surfaced
three more at UTC+9, and those surfaced two more at UTC+14.

`StatusCards/format` is the interesting one. `rollupUpdatesToday` filters on the
UTC calendar day to match the server token cap, while the fixtures were built
on the local day. West of UTC that lands `iso(0)` in the previous UTC day for
the stretch between UTC midnight and local midnight - about seven hours a day
at UTC-7 - and east of UTC+12 "today at local noon" is already yesterday in UTC
outright. Either way the rows it means to count drop out. A run crossing
midnight UTC splits the same way.

Fixes #11476.

Deliberately left: IssueProperties.test.tsx:1515-1517 still pin the minute of
three timestamps against a UTC fixture. They pass at every offset tried,
including UTC+5:45, and the minute there is load-bearing - it distinguishes
Created from Started from Completed - so it wants more care than mechanical
anchoring.

Full ui suite 4113 pass. The TZ pin added by #11480 is still in place here and
is now redundant; #11508 removes it, stacked on this branch so it cannot land
without the anchoring it depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:22:45 -07:00
Tonio 0817fbad92
fix(ui): scope invite membership checks to the signed-in account (#11417)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Which companies a person belongs to is an authorization fact the
server owns, and the UI caches the answer under a single `["companies"]`
key
> - That cache entry carries no account identity, and `main.tsx` sets
`staleTime: 30_000` for every query, so for thirty seconds after a
sign-in the previous account's list is served with no request at all
> - The invite landing page reads that list to decide whether the person
is already a member of the inviting company
> - A list that arrives with no loading state and no error therefore
looks authoritative while describing somebody else
> - This pull request makes the page trust only a list it fetched
itself, for the account signed in now
> - The benefit is that a membership decision stops depending on cache
freshness, which nothing in the app guarantees

## Linked Issues or Issue Description

No public issue exists. Refs #11380, #11382. The problem follows.

**What happened?**

`InviteLanding` read the shared `["companies"]` cache entry as proof of
membership in two places:

- The post-sign-in redirect called
`fetchQuery(companiesListQueryOptions)`, which returns the cached entry
without a request while it is inside the app-wide `staleTime`.
- An effect cleared the pending invite token whenever the cached list
contained the invited company.

Neither checked that the list belonged to the account signed in now. A
second account signing in on a warm tab, or a session that lapses
server-side, is enough to reach both.

**Expected behavior**

The page decides membership from a company list fetched for the current
session.

**Steps to reproduce**

1. On a self-hosted instance in `authenticated` mode, sign in as account
A, which belongs to company X.
2. Within thirty seconds, open an invite link for company X and sign in
as account B, which does not belong to it.
3. The page reads A's cached list, finds company X, and treats B as
already a member.

**Paperclip version or commit**

`master` at `2a4b4bc63`.

## What Changed

- `ui/src/pages/InviteLanding.tsx` — the membership query sets
`staleTime: 0` so it revalidates on mount, and the verdict is withheld
until that fetch lands, keyed on `isFetchedAfterMount`. The
token-clearing effect and the "already a member" branch both read
through that gate.
- `ui/src/pages/InviteLanding.tsx` — the post-sign-in path cancels
anything still in flight for the previous session, then forces a fetch
for the new one with `staleTime: 0`.
- `ui/src/pages/Auth.tsx` — sign-in resets the companies query instead
of invalidating it. Invalidation leaves the previous account's list
readable, and its fetch running, until the refetch returns.
- `ui/src/pages/InviteLanding.test.tsx` — coverage for the warm-cache
case, the token-clearing effect, and the `local_trusted` exemption.

### `local_trusted` is exempt

Those instances have no accounts, so the shared list is the only
identity there is. `membershipIsAccountScoped` is false there and the
gate stays open.

### Rebased onto the account-keyed cache

#11488 landed while this was open and keys the company list by account,
so the page can no longer reach another account's list at all. Two
things changed here as a result:

- The post-sign-in read now calls `fetchCompanyListForCurrentAccount`,
which replaces the `cancelQueries` plus forced `fetchQuery` this PR
originally carried. The helper is strictly stronger: it detaches the
in-flight `/companies` request inside the query function, and it
resolves the account identity past the session invalidation immediately
above rather than trusting the session entry still in the cache.
- The observer reads through `useCompanyListQuery`.

The mount-scoped `isFetchedAfterMount` gate is **kept**, not removed.
Its purpose has narrowed — cross-account leakage is now structurally
impossible, so what remains is holding the verdict until this page has a
list rather than acting on a pending one. It is still load-bearing:
disabling it fails two tests here. Removing a defense in the same change
that rebases onto a new foundation is the wrong order; that is a
follow-up once the keying has proven itself.

`Auth.tsx` can safely reset, because it navigates away on success and
`InviteLanding` mounts fresh afterward. Measurements of exactly when
that rewind does and does not bite are in
[#11380](https://github.com/paperclipai/paperclip/pull/11380#issuecomment-5300984911).

## Verification

- `InviteLanding.test.tsx`, `Auth.test.tsx`, `companies-query.test.ts`,
`CompanyContext.test.tsx` together: **51 passed**, run twice.
- `pnpm tsc -b`: clean.
- `InviteLanding.test.tsx` and `Auth.test.tsx` together: 21 passed.

Both failures are pre-existing and unrelated. Each reproduces on a tree
that does not contain this change, in files this change does not touch:

| Failure | Why it fails |
| --- | --- |
| `IssueProperties.test.tsx` | Timezone-dependent: expects `4:08 PM`,
gets `9:08 AM` |
| `StatusCards/format.test.ts` | Time-of-day dependent: "only counts
updates started today" breaks near midnight |

**Not done:** no manual two-account run in a browser. The path needs two
accounts on an `authenticated` instance, which a local dev instance
cannot exercise.

## Risks

Low. The failure direction is a membership verdict withheld for one
extra round trip, which resolves itself; the direction it removes is one
account's membership granted to another, which does not.

It adds one request per invite-page mount, on a query key the app
already uses.

**This does not close the class.** The shared list is still unscoped for
every other consumer. #11380 clears it on sign-out and #11382 handles
the onboarding draft gate; all three are needed, because an account can
change without passing through any one of those paths.

## Model Used

Claude Opus 5 (`claude-opus-5`), through Claude Code. Extended thinking
enabled. Tool use enabled: file read and edit, shell for typecheck and
test runs.

## Checklist

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

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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 21:55:50 -07:00
Tonio 327f59cac2
refactor(ui): key the company list by account (#11488)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Which companies a person belongs to is an authorization fact the
server owns, and the UI caches the answer for speed
> - It cached that answer under one `["companies"]` key with no account
attached, while `main.tsx` sets `staleTime: 30_000` for every query
> - So for thirty seconds after an account change, one person's list
answered questions asked about another, arriving with no loading state
and no error
> - Three separate consumers each grew their own defense against this,
and each was a place to forget one
> - This pull request keys the entry by account, so a list belonging to
someone else is not distrusted but unreachable
> - The benefit is that the protection stops depending on every future
consumer remembering to defend itself

## Linked Issues or Issue Description

No public issue exists. Refs #11380, #11382, #11417, #11430. The problem
follows.

**What happened?**

The company list lived in a single cache entry, `["companies"]`,
carrying no record of which account it was fetched for. Combined with
the app-wide 30s `staleTime`, any read within that window after an
account change returned the previous account's list — from cache, with
no request, no loading state and no error.

Every consumer that treats the list as an authorization fact had to know
this and defend itself:

- `InviteLanding` reads it to decide whether you already belong to the
inviting company (#11417).
- `OnboardingWizard` reads it to decide whether a saved draft belongs to
you (#11382, merged).
- `CompanyProvider` reads it to pick and persist your active company
(#11430).

All three defenses are correct. The problem is structural: the fourth
consumer has to invent a fourth one.

**Expected behavior**

A cached company list can only answer questions about the account it was
fetched for.

**Steps to reproduce**

1. On a self-hosted instance in `authenticated` mode, sign in as account
A, which belongs to company X.
2. Within thirty seconds, have account B become the session in that tab
— a second tab signing in, or A's session lapsing server-side.
3. Any consumer reading the company list receives A's list, and nothing
in the query result indicates it is not B's.

**Paperclip version or commit**

`master` at `ac91b7f3b`, which includes #11430.

## What Changed

- `ui/src/lib/queryKeys.ts` — `companies.list(userId)` replaces
`companies.all` as the list's entry. `companies.all` remains the prefix,
so it still matches for invalidation.
- `ui/src/api/companies-query.ts` — `companyListQueryOptions(userId)`
builds the keyed options; `useCompanyListQuery()` is the only observer
entry point and holds until the session settles, because the key cannot
be built before then; `fetchCompanyListForCurrentAccount(queryClient)`
covers imperative paths; `useAccountIdentity()` exposes the session
identity the key is built from.
- `ui/src/api/companies-query.ts` — the `/companies` detach moved into
the query function.
- `ui/src/context/CompanyContext.tsx` — drops the session-watching
refetch machinery the key now makes unnecessary (`removeQueries`, the
explicit replacement fetch, the awaiting gate). It still clears the live
selection on an account change, because that is component state and does
not change key with the query.
- `ui/src/pages/InviteLanding.tsx`,
`ui/src/components/OnboardingWizard.tsx` — read through the
account-aware API.
- Tests — the account-keyed guarantee, prefix invalidation still
reaching the list, the detach inside the query function, and updates
where suites seeded the old shared key.

### The existing defenses are deliberately left in place

The per-consumer gates in #11382, #11417 and #11430 are now belt and
braces. They are also what will catch this refactor if it is wrong
somewhere, so removing them in the same change that moves the foundation
would be the wrong order. Simplifying them is a follow-up, once this has
proven itself.

### Why `retry: 1` appears in CompanyProvider

An earlier measurement on #11430 found a retry on the replacement fetch
changed no outcome, because `removeQueries` made the observer rebind and
issue a second request for free. Keying by account removes that
mechanism and the free attempt with it. The retry now carries the
property the incidental refetch used to — a single blip during an
account change should not leave the customer with no companies until
they find "Try again". #11430's test for that property is unchanged and
still passes, which is how the gap was caught.

### A regression this went through, kept for the record

Gating the query on the session settling meant that while the account
was unknown the query was *disabled*, and a disabled query reports
`isLoading: false` with no data — which the provider defaults to an
empty list and reads as "asked, and owns nothing". That is the
destructive branch #11477 had just fixed, reached through a different
door: it would have cleared the customer's stored company on every cold
boot. #11477's test caught it during the rebase. `useCompanyListQuery`
now reports the wait for the account as part of the wait for the list.

### What this does not do

It does not scope the rest of the per-account cache. `["companies",
id]`, stats, and every other account-scoped entry still survive an
account change; that is the cache-lifetime work in #11380.

## Verification

- `pnpm vitest run` in `ui`: **4018 passed, 1 failed**.
- `pnpm tsc -b` in `ui`: clean.
- `companies-query.test.ts`: 6 passed. `CompanyContext.test.tsx`: 17
passed. `OnboardingWizard.test.tsx`: 13 passed.
`InviteLanding.test.tsx`: 13 passed.

The failure is the pre-existing timezone-dependent
`IssueProperties.test.tsx`, fixed by #11478.

Two behaviours are asserted rather than assumed, because the refactor is
only safe if they hold: that invalidating the `companies` prefix still
marks the account-keyed list stale (19 call sites depend on it), and
that the query function detaches the in-flight `/companies` request
before fetching.

**Not done:** no manual two-account run in a browser. The path needs two
accounts on an `authenticated` instance, which a local dev instance
cannot exercise.

## Risks

Moderate, and worth reading before approving.

**It touches `InviteLanding.tsx`, which #11417 also modifies**, so one
of the two will need a rebase — the conflict is mechanical (both change
how the same query is read).

This was #11481, which GitHub closed automatically when its base branch
(#11430's) was deleted on merge; reopening a pull request whose base
branch is gone is not permitted, so it continues here against `master`
with the same head and the same review already recorded on #11481.

**The list now waits for the session query.** The key cannot be built
before the account is known. In the app the session is already fetched
at boot by many components, so this is a dependency rather than an extra
request, but it does serialize: on a cold boot the list waits for the
session to land. Every test that renders a company-list consumer now
needs a session in the cache, which is why several suites gained a seed.

**A missing mock surfaces as a passing gate rather than an error.** The
detach inside the query function meant suites whose `companiesApi` mock
lacked `detachInflightList` had their query function throw, which read
as "decided" in the onboarding gate and mounted the wizard early. Fixed
in the affected suites; worth knowing as a failure mode.

## Model Used

Claude Opus 5 (`claude-opus-5`), through Claude Code. Extended thinking
enabled. Tool use enabled: file read and edit, shell for typecheck and
test runs.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 21:23:56 -07:00
Dotta fd472d02ba
Show ordered live blocker work in task chat (#11487)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The task view shows operators why work cannot continue
> - The redesigned task thread now shows direct and ultimate blockers
> - But it does not show the ordered task queue while a blocker chain
has live work
> - This pull request adds a compact ordered live-work queue to the
redesigned thread
> - The benefit is that operators can see completed, running, and queued
dependencies without opening the larger legacy notice

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The redesigned task thread blocker summary is improved. The merged
predecessor is #11456.

**Current behavior**

The redesigned task thread shows compact direct and ultimate blocker
links. It does not show the ordered queue when the blocker tree has live
work. The legacy task view shows this queue in a larger notice.

**Proposed behavior**

Show a compact blue live-work queue at both ends of the redesigned task
thread. Order completed tasks first, then running tasks, then queued
tasks. Show a live terminal leaf as `Now running`. Return to the amber
blocker links when no live dependency remains.

**Reason and benefit**

Operators can see the active dependency order without leaving the
redesigned task view. The compact presentation preserves the new
thread's low-chrome layout.

**Breaking changes**

None. The change only adds UI for blocker data that the task view
already receives.

## What Changed

- Shared the live blocker ordering helper between the legacy notice and
the redesigned task thread.
- Added compact ordered dependency links at the top and bottom of the
redesigned thread.
- Added a separate `Now running` link for a live terminal blocker leaf.
- Preserved the compact amber blocker rows when live work is not
present.
- Added component tests and a Storybook state for the new presentation.

## Verification

- `pnpm exec vitest run ui/src/components/TaskChatThread.test.tsx
ui/src/components/IssueBlockedNotice.test.tsx`
- `pnpm check:token-gates`
- `pnpm -r typecheck`
- `pnpm test:run`
- `pnpm build`
- `pnpm --filter @paperclipai/ui build-storybook`
- Captured and reviewed the new Storybook state in a headless browser.

## Risks

- Low risk. The queue appears only for blocked tasks whose blocker
attention state is `covered` and whose dependency set contains live
work.
- The API does not provide an explicit queue position. The UI preserves
the existing legacy ordering rule: completed, running, queued, then
numeric task identifier.

> 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. The runtime does not expose the exact snapshot or
context-window size. Reasoning, code execution, repository tools, and
browser automation 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 checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-16 15:10:05 -04:00
Tonio ac91b7f3b2
fix(ui): scope the company selection to the signed-in account (#11430)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Every company-scoped screen reads the active company from
`CompanyProvider`, which picks one from the `["companies"]` list and
remembers it in localStorage
> - That cache entry is shared app-wide and carries no account identity,
so it survives a change of account in the tab
> - The provider therefore auto-selects from whatever list is cached,
which can belong to the account that just went away
> - This pull request makes the provider watch the account and refuse to
derive a selection from a list fetched for a different one
> - The benefit is that the app stops pointing at a company the
signed-in account may not be able to see

## Linked Issues or Issue Description

No public issue exists. Refs #11380, #11382, #11417. The problem
follows.

**What happened?**

`CompanyProvider` auto-selects a company from the shared `["companies"]`
cache entry and writes that id to `localStorage`. Nothing ties that
entry to an account. When the account changes in the tab, the previous
account's list is still served, so the provider can select — and persist
— a company belonging to the account that just went away. Company-scoped
screens then render against a company the current account may not be
able to see.

Signing in through `Auth.tsx` invalidates the entry, so the in-app
sign-in path is covered. Two paths are not: a session that lapses
server-side, and a second account signing in on another tab. The
sign-out sweep in #11380 does not cover them either, because neither
presses the sign-out button.

**Expected behavior**

The company selection is derived only from a company list fetched for
the account that is signed in now.

**Steps to reproduce**

1. On a self-hosted instance in `authenticated` mode, sign in as account
A, which belongs to company X.
2. In a second tab, sign in as account B, which does not belong to
company X.
3. Return to the first tab. The session query refetches and reports
account B, while the company list is still account A's.
4. The provider keeps company X selected and leaves its id in
`localStorage`.

**Paperclip version or commit**

`master` at `6542ad1f4`.

## What Changed

- `ui/src/context/CompanyContext.tsx` — the provider observes
`queryKeys.auth.session`. On a change of session user it clears the live
selection, removes the shared company list, and holds auto-select until
a list fetched for the new account lands. The stored id is left alone on
purpose: `resolveBootstrapCompanySelection` re-validates it, so an
account signing back in keeps its company while an unrelated account
cannot inherit it.
- `ui/src/context/CompanyContext.tsx` — an errored list is treated as
undecided rather than as "no companies". With `retry: false` a single
network blip sticks, and the empty-list branch read it as proof the
account owns nothing and cleared the stored selection.
- `ui/src/api/client.ts` — new `detachInflightGet(path)`. GET coalescing
keys on the request path alone, so a `/companies` request issued under
the previous session could be joined by the replacement fetch and answer
it with the previous account's companies. Detaching leaves that request
to settle for its own callers and makes the next call issue a fresh one.
- `ui/src/api/companies.ts` — `companiesApi.detachInflightList()` wraps
that for the list path.
- `ui/src/context/CompanyContext.tsx` — `companyListUnavailable`
separates "no usable list because a request failed" from "this account
owns nothing", and `retryCompanies` gives a recovery action that
fetches. Both are derived from the query rather than tracked beside it;
a second copy of "did the last attempt succeed" drifted out of step
during review, reporting a failure over a later empty list that was
simply the truth.
- `ui/src/components/SidebarCompanyMenu.tsx` — renders "Couldn't load
companies" and a Try again item in place of "No companies", which is a
claim about the account that a failed request cannot support. This is
the menu `Sidebar` mounts, so it is the only place a customer can act on
the failure.
- `ui/src/components/CompanySwitcher.tsx` — the same treatment. The
application does not render this component (its only mount is a
Storybook story), so it is kept in step rather than relied on.
- `ui/src/context/CompanyContext.test.tsx`,
`ui/src/components/SidebarCompanyMenu.test.tsx`,
`ui/src/api/client.test.ts` — coverage for the account switch, a
same-account re-observation not churning, the detached GET, the failed
replacement and its recovery, a single blip self-healing, unavailability
not outliving the failure, and the sidebar rendering the recovery action
for a failure but plain "No companies" for an account that owns nothing.

### No `retry` override on the replacement fetch

The obvious fix for a failed replacement is a retry, and it is not
load-bearing here. A transient failure already gets a second attempt:
the observer rebinds to a fresh query on the render those state updates
schedule, and issues its own request — measured as two attempts with or
without the option. Retries would only add failed round trips before a
real outage is reported, and the outage is what needs a way out, which
is what `companyListUnavailable` and `retryCompanies` provide.

### Why `removeQueries` here, and why that does not generalise

Removal notifies no observer. What rebinds them at this call site is the
render the surrounding state updates schedule; every observer re-binds
to a fresh query on the next render. A caller without that guarantee
would leave mounted observers serving the previous account's value, so
this is not a pattern to lift elsewhere — the sign-out sweep in #11380
must use `resetQueries` instead, and its measurements are at
[#11380](https://github.com/paperclipai/paperclip/pull/11380#issuecomment-5300984911).

The inverse caveat holds for a local reset under an observer that stays
mounted, which is why #11417 and #11382 avoid `resetQueries`.

## Verification

- `pnpm vitest run` in `ui`: **4014 passed, 1 failed**.
- `pnpm tsc -b` in `ui`: clean.
- `CompanyContext.test.tsx`: 16 passed. `SidebarCompanyMenu.test.tsx`:
15 passed. `client.test.ts`: 9 passed.

The failure is pre-existing and unrelated: `IssueProperties.test.tsx`
expects `4:08 PM` and gets `9:08 AM`, a timezone-dependent assertion. It
reproduces on a tree without this change, and #11478 fixes it.

Each new test was confirmed to fail against the implementation it
covers, by reverting that change and re-running rather than by assuming.
The account-switch test fails without the fix (the selection stays on
the previous account's company and no refetch is issued); the
flag-clearing test fails without its clause (an empty list keeps reading
as "couldn't load").

**Not done:** no manual two-account run in a browser. The path needs two
accounts on an `authenticated` instance, which a local dev instance
cannot exercise.

## Risks

Low. The failure direction is a company selection withheld for one extra
round trip, which resolves when the list arrives. The direction it
removes is one account's company selected and persisted for another.

It adds one company-list request per account change, on a query key the
app already uses. It adds no request at boot: the session query it
observes is already fetched app-wide.

**This does not close the class.** Company-scoped entries other than the
list — `["companies", id]`, stats, and the rest of the per-account cache
— still survive an account change. That is the cache-lifetime work in
#11380, not this provider's.

## Model Used

Claude Opus 5 (`claude-opus-5`), through Claude Code. Extended thinking
enabled. Tool use enabled: file read and edit, shell for typecheck and
test runs, and a scratch vitest harness to measure `removeQueries` and
`resetQueries` notification behaviour against the installed
`@tanstack/query-core` 5.101.4.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 11:51:03 -07:00
Dotta 10d0555189
fix(interactions): authorize resolvers consistently (#11376)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Issue interactions give agents and people a structured decision
record.
> - Resolver routes used different authorization rules.
> - Some routes blocked valid agents, including task watchdogs with
normal issue access.
> - The API did not show who could resolve a pending interaction.
> - This pull request gives every interaction kind one resolver policy
evaluator.
> - The benefit is a clear decision path with consistent governance and
company isolation.

## Linked Issues or Issue Description

Fixes: #8087

Refs: #7403

Related PR: #11082 proposes board-only confirmation rules. This change
keeps human-only review as an explicit policy.

**What happened?**

Agents could create issue interactions. Some resolver routes still
required board access.

This left valid agent confirmations pending. Task watchdogs could see
the same problem without board identity.

**Expected behavior**

Every interaction kind must use one resolver policy contract.

The contract must support `anyone`, `not_creator`, and `human_only`. It
must also apply all normal governance controls.

**Steps to reproduce**

1. Create a `request_confirmation` interaction as an agent.
2. Resolve it with another authorized agent.
3. Observe the board-only denial.

**Paperclip version or commit**

The problem exists on `master` before this change.

**Deployment mode**

Local development with `pnpm dev`.

## What Changed

- Add canonical policies for `anyone`, `not_creator`, and `human_only`.
- Use one server evaluator for every interaction kind.
- Apply named addressees, company limits, review rules, and task
watchdog scope.
- Charge cross-issue resolutions to the existing per-run action limit.
- Return the effective resolver audience in attention and interaction
data.
- Show the audience, governance choices, and denial reasons in the board
UI.
- Add telemetry, API documents, product documents, and regression
fixtures.
- Add migration provenance for safe legacy behavior.
- Make migration `0218` safe for complete replays and partial prior
runs.

## Product Rules

- An interaction records a response. It does not grant authority for the
next action.
- `anyone` lets any authorized issue participant respond.
- `not_creator` requires a responder other than the interaction creator.
- `human_only` requires an authorized person.
- A named addressee, company policy, or governed action can narrow the
audience.
- These controls cannot widen the audience.
- A task watchdog uses the same rules as an ordinary agent.
- A task watchdog does not receive board authority.
- An agent resolution on another issue uses the shared cross-issue
action limit.
- Legacy pending interactions keep their earlier restrictions.
- The UI shows the effective audience and a permanent denial reason.

## Verification

- `pnpm --filter @paperclipai/db check:migrations`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm exec vitest run
packages/db/src/issue-thread-interaction-resolver-policy-migration.test.ts`
- The focused PostgreSQL test applies migration `0218` twice.
- The test also completes a partial prior run and preserves existing
provenance.
- The latest GitHub head has 29 successful checks.
- The opt-in Storybook visual check skipped as expected.
- Greptile reports 5/5 with no open comments.

## Risks

- New interaction writes use `anyone` by default.
- Callers must select `not_creator` or `human_only` when they need
stricter review.
- Legacy pending interactions keep the old creator and human
restrictions.
- Migration `0218` fills only missing provenance fields during recovery.
- Cross-issue resolutions can reach the existing action limit.
- The shared evaluator affects every interaction kind.
- Route, service, database, shared contract, and UI tests cover these
rules.

> This work matches the Agent Reviews and Approvals direction in
`ROADMAP.md`. It does not duplicate a planned item.

## Model Used

OpenAI Codex, GPT-5. The runtime does not expose the exact deployment ID
or context window.

The agent used reasoning, repository tools, shell commands, 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 checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either linked public issues or described the issue with the
required labels
- [x] I have not referenced internal Paperclip issues or links
- [x] My branch name describes the change and contains no internal
ticket id
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented the risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open comments
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-16 13:46:50 -05:00
Dotta d6acb48551
feat(ui): show a calm in-flight notice when a live run is on the issue (#11423)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When an agent run ends without a recorded disposition, Paperclip
raises a "missing disposition" handoff so the work does not stall
silently
> - The issue page shows that handoff as an amber alarm: "This task
still needs a next step."
> - The server tells the UI whether the issue has a live continuation,
and an earlier change used that flag to hide the alarm while a
correction run is active
> - Hiding it removed the false alarm but replaced it with nothing, so a
reader cannot tell "nothing is wrong" from "nothing is tracked"
> - This pull request puts a quiet informational line where the alarm
was, and links the live run
> - The benefit is that the page stays honest in both states: it is calm
while an agent works, and it is loud only when the issue is really stuck

## Linked Issues or Issue Description

No public GitHub issue exists for this gap. Description follows
`.github/ISSUE_TEMPLATE/enhancement.yml`:

**What existing behavior does this improve?**

The missing-disposition handoff notice on the issue page. It is the
amber banner that reads "This task still needs a next step."

**Subsystem affected**

Web UI — `ui/src/components/IssueBlockedNotice.tsx`.

**Current behavior**

An issue with an outstanding missing-disposition handoff shows nothing
at all in the blocked-notice slot while a correction run is live.
`IssueBlockedNotice` calls `isSuccessfulRunHandoffRequired()`. That
helper returns `false` when `successfulRunHandoff.hasLiveContinuation`
is set. The component then renders no handoff content. Two tests
asserted the empty render.

**Proposed behavior**

The page states, quietly, that a correction run is in progress. It also
states that the alarm returns if the run stops without choosing a next
step. The reader can open the live run from that line. The amber alarm
does not change when no run is live.

**Reason and benefit**

Silence and "healthy" look the same. A user who saw the alarm earlier
cannot tell whether the handoff was resolved, whether the alert was
withdrawn, or whether an agent is working on it now. One muted line
removes that ambiguity. It also keeps the loud state meaningful, because
the alarm now appears only when the issue is really stuck.

**Breaking changes**

None. The change is presentational and adds no API or data-shape change.

## What Changed

- Added `SuccessfulRunHandoffInFlightNotice` to
`ui/src/components/IssueBlockedNotice.tsx`. It renders a muted row with
a pulsing live dot and this copy: "A correction run is in progress — the
agent is working. This alert returns if the run stops without choosing a
next step."
- The notice links the live run when the server sends `liveRunId` and
the handoff has an `assigneeAgentId`. It shows the short run id as plain
text when no agent id is available, and it shows no run reference when
`liveRunId` is absent.
- Liveness reads either the server `hasLiveContinuation` flag or the
fresher client `liveIssueIds` set. This matches the rule that already
suppressed the alarm.
- The amber alarm is unchanged when no live continuation exists. The
unpromoted scheduled-retry carve-out still shows the alarm, so the
"Retry now" control stays reachable.
- The calm line also renders above the blocker notice when an issue has
blockers and a live run at the same time.
- Storybook: added `InFlightNotice` and `LivenessComparison` stories to
`ui/storybook/stories/successful-run-handoff.stories.tsx`, and removed a
duplicated panel from the overview story.
- Tests: the two cases that asserted an empty render now assert the calm
line. New cases cover a missing `liveRunId`, a missing agent id, a
handoff that is not required, and the two "alarm is unchanged" guards.

## Verification

Run the component and helper suites from `ui/`:

```
cd ui && NODE_ENV=test npx vitest run \
  src/components/IssueBlockedNotice.test.tsx \
  src/components/IssueChatThread.test.tsx \
  src/components/IssueChatThreadSystemNotice.test.tsx \
  src/lib/successful-run-handoff.test.ts
```

Result: 4 files, 111 tests, all pass.

Also run:

- `cd ui && npx tsc -b --force` — clean.
- `node scripts/check-token-gates.mjs` — all gates clean.

Manual check in Storybook (`pnpm --dir ui storybook`), story
`Paperclip/Successful Run Handoff → Liveness Comparison`:

- The alarm panel keeps its 4 remediation bullets, its amber surface,
and its run chips.
- The calm panel shows one 39 px muted row, no bullets, and a working
link to the live run.
- Measured contrast of the calm text against its rendered surface:
4.58:1 in light mode and 6.52:1 in dark mode. Both pass WCAG AA for
normal text.

## Risks

Low risk. The change is limited to one presentational component and its
stories. It adds a render path where the component previously returned
`null`, so a surface that expected an empty render now shows one muted
row. No server, API, or data-shape change. The amber alarm path and the
scheduled-retry carve-out are covered by tests that assert the calm line
does not appear.

## Model Used

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

Co-authored-by: ClaudeCoder <claudecoder@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-16 13:20:33 -05:00
Tonio 92047cac46
test(adapter-utils): make the next sandbox flake diagnosable (#11483)
`execution-target-sandbox` has failed twice in CI and not once in several
hundred local runs. This does not fix it. It makes the next occurrence carry
its own evidence, because a third unreproducible failure would teach nothing.

The observed signature was an empty stdout with exit code 0 - the child exited
cleanly having produced nothing, which is what a lost stdin frame looks like
from the test's side. Three mechanisms were checked and ruled out rather than
assumed: the helper resolving on `exit` rather than `close` (a 200-iteration
probe produced no truncations, and the failure was empty rather than partial);
the wrapper reporting exit before stdout drains (it already listens on
`close`); and frame writes racing (the stream wrapper's `writeEvent` is
synchronous and sequence-numbered).

Two candidates remain and the runtime tree separates them. A stdin queue frame
still present means the host wrote it and the wrapper never consumed it; a
drained queue with no output means it was consumed and the reply was lost on
the way back. The report prints that tree, both proxy streams, the exit code,
and the elapsed time - the last because the bridge and proxy run on 5s budgets
that are generous locally and tight on a runner sharing a box with 19 other
lanes.

Timeouts are deliberately unchanged. Raising them would probably make the
symptom go away, which is the reason not to do it blind.

The first revision capped the tree walk one level above the queue frames, so
"the queue is empty" and "the walk never looked" printed identically - the
distinction the report exists to make. Caught in review. Verifying that the
reporter printed something was not enough; it had to print the thing that
discriminates, which is now checked by planting a frame and forcing the
assertion.

adapter-utils typecheck clean; 44 pass, stable across repeated runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:47:35 -07:00
Tonio add65ba4a2
test(ui): make the suite pass outside UTC (#11480)
The ui suite was green in CI and red on a clean checkout in any other
timezone. GitHub's runners default to UTC, so nothing ever reported it. A
contributor elsewhere sees two failures on their first run, which reads as
"this project is broken" rather than "your clock differs from the runner's".

Two independent causes.

`IssueProperties` supplies a UTC instant and asserts on the local-time string
the UI renders from it - "2026-07-17T16:08:00.000Z" is expected to read
"Today, 4:08 PM". That holds only where local time is UTC. Pinned with
`env: { TZ: "UTC" }` rather than rewritten: those assertions are about what a
person sees, and "4:08 PM" is worth more to a reader than an expectation
computed from the same formatter the component uses, which would pass whatever
that formatter did.

`StatusCards/format` was wrong in two ways at once, and the pin hides only one,
so it is fixed directly. `rollupUpdatesToday` filters on the *UTC* day
boundary, while the test built fixtures from local noon on the real clock.
East of UTC+12, "today at local noon" is already yesterday in UTC and the rows
the test means to count are filtered out; and any run crossing midnight UTC
lands `iso(0)` and the function's default `now` on different days. The
fixtures now come from a fixed instant, passed as `now` - the parameter exists
for this, and the sibling test already used it.

Each fix was confirmed load-bearing by removing it under TZ=Pacific/Auckland.
Without the pin, IssueProperties fails; without the fixed instant, StatusCards
fails even with the pin removed, so neither rides on the other.

Full ui suite 4017 pass, 0 fail, in UTC, Pacific/Auckland and Asia/Kolkata.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:15:38 -07:00
Dotta 9e9f744f58
Show blocker links in the task chat (#11456)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip helps operators supervise agent work through tasks and
task threads.
> - The redesigned task thread shows the current work and its state.
> - A blocked task did not show the dependency that prevented progress.
> - Operators had to leave the thread to find the direct and final
blockers.
> - This pull request adds compact blocker links at the top and bottom
of the task thread.
> - The benefit is that operators can identify and open the relevant
tasks without adding a large notice to the thread.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The redesigned task thread did not show which task directly blocked the
current task or which task ultimately blocked its dependency chain.

**Subsystem affected**

`server/`, `packages/shared/`, and `ui/` task-blocker presentation.

**Current behavior**

A blocked task can open in the redesigned thread without a visible
dependency link at the top or bottom of the conversation.

**Proposed behavior**

Show one compact amber row for the direct blocker. Show a second row for
the selected final blocker when one exists. Render the rows at both ends
of the thread.

**Reason and benefit**

Operators can see the reason for the blocked state and open the relevant
task from the conversation. The compact rows preserve thread density.

**Breaking changes**

None. The new blocker-attention fields are optional. Existing clients
remain compatible.

## What Changed

- Added a compact task-chat component for direct and selected final
blocker links.
- Added the blocker rows to the top and bottom of populated and empty
task threads.
- Added link-ready blocker-attention details so an intermediate selected
task stays on its correct direct chain.
- Included blocker-link changes in the thread content key so pinned
threads follow a newly added bottom row.
- Added component, scrolling, server contract, and Storybook coverage
for the new states.

## Verification

- `pnpm exec vitest run ui/src/components/TaskChatThread.test.tsx
server/src/__tests__/issue-blocker-attention.test.ts` (38 tests passed)
- `pnpm --filter @paperclipai/shared typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm check:token-gates`

## Risks

- Low risk. The rows only render while the task status is `blocked` and
an unresolved blocker is available.
- Long titles are truncated to keep each blocker on one line. The full
task label remains available in the link title.
- Older server payloads keep the original leaf-selection behavior
because the new sampled details are optional.

> 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 with GPT-5. The run used tool-enabled reasoning and code
execution. The context-window size was not exposed to the run.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-16 13:09:17 -04:00
Tonio e384d0a2bd
fix(ui): keep the stored company when the company request fails (#11477)
An error is not an answer, and this branch is destructive.

`companiesListQueryOptions` sets `retry: false`, so a request that fails before
ever succeeding leaves `data` undefined - which `CompanyProvider` defaults to
`{ companies: [], unauthorized: false }`. That is indistinguishable from "this
account was asked, and owns nothing", so `shouldClearStoredCompanySelection`
returned true and the effect removed the customer's stored company. With
`refetchOnWindowFocus: true`, a blip on focus during a cold load was enough,
and the next visit drops them onto whichever company sorts first.

The predicate now takes `errored` and refuses to clear on it. Required rather
than optional, so the compiler made both existing call sites state their
answer instead of inheriting a default.

Not clearing costs nothing: a stored id that no longer resolves is ignored by
`resolveBootstrapCompanySelection`, which checks it against the current list
before using it. Clearing wrongly costs the customer's selection, which cannot
be recovered.

Scoped deliberately. This file had been described as carrying the same defect
as the onboarding draft gate and the sign-out sweep, and that was overstated.
Those two *trusted* a stale list to answer "does this account own this
company?". This one validates membership against the current list and only
picks a default, so a stale list here self-corrects rather than leaking. The
failed-request branch is the part that is genuinely wrong, and it is the only
part changed. The transient re-decision during a background refetch is real,
self-correcting, and left alone.

Tested at both levels, because the predicate alone would not have caught it:
the provider is what defaults a failed request to an empty list, so the wiring
is where the decision goes wrong. Removing the guard fails both.

ui typecheck clean; full ui suite 4016 pass, with only the timezone-dependent
IssueProperties failure already present on master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:58:46 -07:00
Tonio 0023c5c4a6
refactor(ui): record why sign-out resets rather than removes (#11473)
Comment only. Salvaged from `claude/sign-out-cache-note`, written while #11380
was in progress and never opened as a PR; that branch is deleted with this.

#11380 landed the reasoning for `resetQueries` but not the evidence, and not
the part that stops someone reversing it later.

The choice was measured rather than argued. Against query-core 5.101.4,
removal produced 0 notifications and left the observer holding the signed-out
account's session; reset produced 3 and null. The consequence of the former is
not only a stale read - CloudAccessGate's redirect fires on the session going
empty, so it never runs.

The opposite advice really does hold for a local reset of a key an observer is
still mounted against: reset rewinds the update counters `isFetchedAfterMount`
derives from while that observer keeps its bind-time baseline, so anything
gated on the flag withholds forever. Sign-out is not that case, because it
resets the session too and the consumer unmounts on the redirect.

Two sessions reached opposite recommendations on this API within a day, both
correct about different situations, which is the kind of thing a later reader
re-litigates without a note in the file.

The first revision of this note named the wrong consumers - InviteLanding and
the onboarding draft gate, neither of which reads `isFetchedAfterMount`.
`AppsConnect` is the only one on master and is what it names now.

ui typecheck clean; 13 pass across the sign-out suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:44:36 -07:00
Nicky Leach cd501499a2
test: add ACPX run lifecycle characterization baselines (#11461)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The adapter runtime starts, turns, settles, and composes ACPX runs
> - Recent lifecycle corrections changed several order and cleanup rules
> - Those rules need regression coverage before the planned engine
refactor
> - This pull request adds characterization suites for the corrected
behavior
> - The benefit is a clear test baseline for the next refactor

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The ACPX adapter runtime and server heartbeat lifecycle need stable
regression coverage for their current corrected behavior.

**Subsystem affected**

Cross-cutting (multiple of the above): `packages/adapter-utils` and
`server` test suites.

**Current behavior**

The runtime has corrected rules for startup, turns, settlement, composed
results, and heartbeat terminalization. The repository lacks a single
characterization baseline for these rules.

**Proposed behavior**

Keep the current lifecycle rules pinned by five test suites. Let the
later engine refactor change behavior only when it updates these tests
with a clear reason.

**Reason and benefit**

The suites expose order, cleanup, transport, timeout, retry, result, and
lease-release changes during the refactor. They also record one known
latent defect as current behavior.

**Breaking changes**

None. This pull request adds tests only.

## What Changed

- Add startup characterization coverage for commands, launch values,
session fingerprints, sync order, bridge overlap, and cleanup paths.
- Add turn characterization coverage for inputs, events, transports,
timeout and cancel behavior, retry rules, errors, and usage.
- Add settlement characterization coverage for teardown, adapter
sync-back, workspace restore order, native sync, and error policy.
- Add composed-run characterization coverage for result forms,
finalization sets, and host-lane warm save and warm hit behavior.
- Add server coverage that checks run terminalization before environment
lease release.

## Verification

- Run `npx vitest run
packages/adapter-utils/src/acpx-engine/startup-characterization.test.ts
packages/adapter-utils/src/acpx-engine/turn-characterization.test.ts
packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts
packages/adapter-utils/src/acpx-engine/composed-run-characterization.test.ts
packages/adapter-utils/src/acpx-engine/execute.test.ts`.
- Run `npx vitest run
server/src/__tests__/heartbeat-run-terminalize-before-release.test.ts`.
- The adapter-utils run passes 178 tests, and the server run passes 4
tests.
- Check `pnpm --filter @paperclipai/adapter-utils typecheck`.
- Check `pnpm --filter @paperclipai/server typecheck`.

## Risks

Low risk. The change adds test files and does not change production
code. One known cold ensure-session cleanup defect remains pinned as
current behavior.

## Model Used

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-15 21:36:59 -07:00
Nicky Leach e52b8a343f
fix: ACP run lifecycle corrections — failure settlement, workspace sync-back, lease cleanup (#11454)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent adapters run ACP sessions and manage runtime, workspace, and
lease resources.
> - Several failure paths left runtime bridges, staged workspaces, or
environment leases active after an error.
> - These leaks reduce run reliability and can leave later runs without
clean resources.
> - This pull request closes the failure paths, applies one teardown
policy, and adds regression tests.
> - The benefit is consistent failure settlement and safer reuse of
agent workspaces and leases.

## Linked Issues or Issue Description

**What happened?**

ACP runs could leave runtime bridges, staged workspaces, or environment
leases active after failures. Claude and Gemini ACP runs did not restore
the sandbox workspace on teardown. Lease release stopped when one lease
returned an error.

**Expected behavior**

Each ACP failure must return an error result and settle its resources.
Teardown must run each step, release leases independently, and restore
the host workspace when the sandbox ends. Pending cleanup leases must
receive bounded retry attempts.

**Steps to reproduce**

1. Run an ACP session that fails after runtime creation or during turn
preparation.
2. Run an ACP session that fails during a warm hit or staged runtime
handoff.
3. Run lease cleanup with more than one lease when the first release
returns an error.
4. Inspect the result phase, teardown calls, workspace state, and lease
metadata.
5. Run the regression suites listed in the Verification section.

## What Changed

- Settle every ACP failure after runtime creation with an error result
and one sandbox.startup span closure.
- Close the ACP runtime and remove warm entries after every pre-turn
failure.
- Run all teardown steps, record teardown errors, release staging leases
in finally, and prevent duplicate teardown.
- Dispose staged runtimes after seam failures and remove borrowed staged
entries with identity guards.
- Add fail-open workspace sync-back teardown for Claude and Gemini ACP
adapters.
- Isolate lease release errors and add bounded retry sweeps for stranded
pending_cleanup leases.
- Atomically claim pending_cleanup retries and clamp attempt readers to
keep the five-attempt bound.
- Default absent provider reusableLeases values to false and align the
fake provider with its runtime declaration.
- Add regression tests for engine, adapter, server, and shared
environment behavior.

## Verification

- [x] `npx vitest run
packages/adapter-utils/src/acpx-engine/execute.test.ts` — 124 tests
passed.
- [x] `npx vitest run
packages/adapters/codex-local/src/server/acp.test.ts
packages/adapters/claude-local/src/server/acp.test.ts
packages/adapters/gemini-local/src/server/acp.test.ts` — 61 tests
passed.
- [x] `npx vitest run server/src/__tests__/environment-runtime.test.ts
server/src/__tests__/heartbeat-pending-cleanup-sweep.test.ts
server/src/__tests__/reusable-leases-default.test.ts
server/src/__tests__/environment-routes.test.ts
packages/shared/src/environment-support.test.ts` — passed.
- [x] All listed suites ran from the repository root.
- [x] GitHub CI completed successfully for
`cfc349c9f232711433897915112a1c52c0e462ca`.
- [x] Greptile completed with a 5/5 confidence score and no blocking
finding.

## Risks

The engine changes affect failure settlement and teardown order across
ACP runs. The server changes add retry state to existing lease metadata
without a schema migration. The adapter changes restore workspaces after
sandbox execution. Regression tests cover the changed paths. GitHub CI
and Greptile passed for the current head.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. This change fixes runtime
reliability and does not duplicate a roadmap feature.

## Model Used

OpenAI GPT-5 Codex. The model used tool-based repository inspection,
GitHub operations, and code review support. The runtime does not expose
a context-window value.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-15 18:52:08 -07:00
Tonio 38752c4e5e
fix(ui): clear account-scoped query caches on sign-out (#11380)
Sign-out invalidated two query keys - the auth session and health - and left
everything else in the cache. `invalidateQueries` also keeps serving the old
data while it refetches, so it was insufficient even for the two it did touch.
The company list was never touched at all, which is how one account's
companies could still be in hand when the next account signed in.

The self-hosted path now resets every account-scoped entry. Scoping is an
allowlist of instance-scoped roots rather than a list of things to clear, so a
query key added later is account-scoped unless someone deliberately says
otherwise - forgetting this file fails closed.

`health` is the only exemption. It carries deployment mode, bootstrap state
and Cloud metadata, nothing account-scoped, and `useCloudInstance` observes it
with `enabled: false` and leaves the fetch to CloudAccessGate. Dropping the
entry would strand every such observer on `null` until the gate happened to
refetch, flipping Cloud instances into their self-hosted rendering mid
sign-out. It is refreshed in place instead.

`resetQueries` rather than `removeQueries`: removal empties the cache without
notifying the observers already subscribed, so a mounted `useQuery` keeps
returning its last result until an unrelated re-render rebuilds it.
CompanyProvider sits above the router and stays mounted across the whole
sign-out and sign-in cycle, so that is the common case here rather than a
corner one. Reset notifies them, so the old data leaves the cache and
everything reading it.

The cloud path is untouched: it is a top-level navigation, and the document
reload builds a new QueryClient with nothing left to clear.

This is the root cause behind the onboarding draft-ownership gate added in
#11382. That gate stays, and its comment now says why: this fix covers the
sign-out button, not the question. An account can change without it - a
session lapsing server-side, a second account signing in on a warm tab, a
caller supplying the company context from somewhere else - so the gate stays
independent rather than deferring to this.

ui typecheck clean; full ui suite 4014 pass, with only the timezone-dependent
IssueProperties failure already present on master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 07:55:14 -07:00
Tonio b38d6ddb81
fix(onboarding): choose the launcher's step from the company's mission too (#11429)
The last of the three ways into onboarding for a company that already exists.
The route resolver and the dashboard both pick the step from whether the
company already has its mission; the "Add Agent" card on `/{prefix}/onboarding`
hardcoded the mission step. So the one entry point whose own copy reads "Add
another agent to X" was the one that stopped to ask X for the mission it
already had.

It now calls `onboardingStepForCompany`, like the other two. `matchedCompany`
moves above the early return because a hook cannot be called after it.

An unsettled or failed lookup still reads as "no mission" and costs the step,
which the customer can answer - the same fail-open rule the other callers
follow, and safe now that confirming the mission updates the company's existing
goal rather than adding a second one.

`OnboardingRoutePage` is exported so this can be driven directly. The
alternative was the whole `<App>` route table, which is a much heavier harness
for a question about one button's argument.

Four cases, and the first fails against the hardcoded step. The button lookup
asserts it matched something before clicking, because a lookup that silently
matches nothing turns the click into a no-op and the test into decoration.

This is the last piece of #11259 that had not landed.

ui typecheck clean; full ui suite 4010 pass. The one failure, in
IssueProperties, is timezone-dependent and reproduces on master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:54:47 -07:00
Tonio 6542ad1f4d
fix(onboarding): carry an existing company's mission into the wizard (#11416)
Ports the substance of #11259, which predated the recent onboarding work and
was still open. One of the parts it solves is a regression #11352 introduced.

A company that already has its mission opens on the agent step, which is the
point of #11352. What that missed is that the mission field is filled only by
the step being skipped, and that the field is not decoration:
`composeCeoInstructions` seeds the lead agent's instructions from it, and the
Review checklist reads it. So every Cloud-seeded company hired its lead agent
with no Mission line at all, having been routed there precisely *because* it
had a mission. Before #11352 those companies dead-ended on the mission step; a
dead end became a quiet data loss, which is worse, because it completes.

`selectExistingCompanyMission` reads the company's own goal back into the shape
the mission field holds, and the wizard hydrates from it - only when the field
is empty, so a customer editing their mission is never overwritten by the
stored copy. The marker recording that hydration travels with the field it
describes, cleared wherever `companyGoal` is.

`isExistingCompanyMissionUnresolved` holds the hire while that read is
outstanding, counting an in-flight refetch over cached goals as unresolved.
That is the rule #11382 settled on a day earlier for a different consumer -
`isFetching`, not `isLoading`, because retained data is not an answer to the
question being asked now. #11259 had it first, on 11 August.

`canGoBackFromOnboardingStep` and `canJumpToOnboardingStep` bound how far back
a run can walk by the step it entered on. The Back button already applied that
rule inline; the progress bar applied only the "already completed" half, so a
run holding a company could still jump to step 1 - the step whose job is to
create one. The entry step is captured once, when the wizard opens, for the
same reason the step itself is.

`planMissionPersistence` came with them and turned out to be required rather
than tidying. Hydration sets `createdCompanyGoalId` from the company's existing
goal, and confirming the mission read that id as "already written" and skipped
the write, discarding the customer's edit. That skip was safe only while the id
could arrive one way - by writing. A goal in hand now means update it.

Each piece was checked by removing it and confirming a specific case fails.
The hydration case asserts on `saveInstructionsFile`'s content, the actual
consumer, rather than on the mission textarea, because the entry path never
renders that field and the navigation bound now prevents reaching it. One
caveat recorded rather than smoothed over: the reopen case fails only when
both marker-clears are removed, since `reset()` also clears the company id and
the next introduction routes through `clearCompanyScopedState`. They are kept
as one invariant rather than one guard plus a coincidence.

ui typecheck clean; full ui suite 4004 pass. Two failures remain, in
IssueProperties and StatusCards/format; both are date-dependent, both
reproduce on master with these changes stashed, and neither file is touched
here.

Co-Authored-By: Jannes Stubbemann <stubbi@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:42:03 -07:00
Tonio 484b1f626c
fix(onboarding): verify draft ownership against a list fetched this session (#11382)
#11370 stopped onboarding restoring a saved draft when the company list had
*errored*. It still trusted the list when the list looked healthy — and the
wider door was exactly that. `main.tsx` sets `staleTime: 30_000` app-wide and
`Auth.tsx` invalidates rather than resets on sign-in, so `invalidateQueries`
keeps serving the previous account's companies with `isLoading` false and no
error at all. On a self-hosted instance, where sign-out does not reload the
page, signing in as a second account in the same warm tab could restore the
first account's draft. No request had to fail.

The wizard now judges ownership against a list it fetched for the current
session: its own `useQuery` on the shared key with `staleTime: 0`, gated so it
runs only when a parseable draft exists and adds no request otherwise.

Every clause of that gate earns its place, and each was verified by removing
it and watching a specific case fail:

- `isSuccess` ties the answer to this session. React Query retains the last
  good `data` when a refetch fails, so after an account switch the retained
  value is the previous account's list; a failed refetch flips status to error
  and this rejects it.
- The `unauthorized` check catches the opposite error. `companiesListQueryOptions`
  folds 401 and 403 into `{ companies: [], unauthorized: true }` rather than
  throwing, so an auth blip arrives as a *successful* empty list and would
  otherwise read as "this account owns nothing" and delete the draft.
- The mount gate keys on `isFetching`, not `isLoading`. `isLoading` is false
  whenever retained data exists, so a refetch over a warm cache mounted the
  wizard undecided — and with the wizard open, the persist effect overwrote
  the customer's own draft with defaults before the answer arrived. It still
  releases on failure, so the "Get Started" dead end stays fixed.
- An unreadable draft is judged, and cleared, before any of the above, and
  does not enable the query at all.

`isFetchedAfterMount` was in an earlier revision and is deliberately not here:
it is true after a failed refetch too, so it rejects nothing `isSuccess` has
not, and no test could distinguish it.

Worth recording how the first defect survived a check. I fault-injected it,
saw a test fail, and concluded the guard worked. It was failing for an
unrelated reason — the inner wizard mounted during the fetch and locked its
state initializers to defaults, so the draft could not appear whatever the
gate decided. Fixing the mount gate exposed the real behaviour. An injection
is only evidence if the failure it produces is the one being claimed.

This narrows onboarding only. The general fault is that a sign-out leaves
account-scoped caches in place, and account changes that skip the button —
a session lapsing server-side, a second account in a warm tab — reach the same
stale list. Tracked separately; this defence should not be removed as
redundant when that lands.

ui typecheck clean; full ui suite 3963 pass, with only the timezone-dependent
IssueProperties failure already present on master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 22:59:41 -07:00
Nicky Leach bc9f70f54c
fix(plugin-daytona): bound the sandbox liveness calls with a per-call timeout (#11408)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox provider plugins run agent work in remote execution
environments
> - The Daytona sandbox liveness read can stay pending when the
connection stops responding
> - A pending read blocks the plugin until a broad host-to-worker limit
expires
> - This pull request adds bounded deadlines to Daytona liveness calls
and clears stale handles
> - The benefit is a fast and clear error when a Daytona connection
stops responding

## Linked Issues or Issue Description

Refs #11341

**What happened?**

The Daytona sandbox liveness read had no per-call timeout. A silent
connection failure left the read pending until the broad host-to-worker
RPC limit expired.

**Expected behavior**

The plugin should stop a liveness call within a defined limit and report
a clear timeout error.

**Steps to reproduce**

1. Create a Daytona sandbox handle.
2. Make the cached handle freshness read never resolve.
3. Run the next sandbox operation.
4. Observe that the operation waits for the outer RPC limit without a
liveness timeout.

**Paperclip version or commit**

`master` before this change.

**Deployment mode**

Any deployment mode that uses the Daytona sandbox provider.

## What Changed

- Add `withLivenessTimeout` with timer cleanup and
`SandboxLivenessTimeoutError`.
- Bound `refreshData` with configurable `livenessTimeoutMs`, which
defaults to 30000 milliseconds.
- Bound sandbox start and recovery calls with the SDK timeout plus a
5000 millisecond margin.
- Reject `livenessTimeoutMs` values above 86400000 milliseconds and
document the setting.
- Evict a cached handle after a failed freshness refresh so the next
operation fetches a new handle.
- Add a test for a never-resolving freshness refresh and the
cached-handle eviction.

## Verification

- Run the Daytona plugin test suite with its package Vitest
configuration.
- Confirm that 150 of 150 tests pass.
- Confirm that the new test reports a bounded timeout and a fresh handle
on the next operation.
- Confirm that GitHub Actions reports green status checks after the pull
request starts.

## Risks

This change adds an early timeout only to Daytona liveness calls. A
value of 0 or less disables the extra bound. The default leaves normal
SDK calls within their expected time limit. The main risk is a timeout
value that is too short for a slow but healthy connection.

## Model Used

OpenAI Codex, GPT-5. The model used tool calls and code execution. The
model supplied the PR handoff and did not author the code in this pull
request.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-14 22:35:06 -07:00
Nicky Leach fdb9a4880d
fix(security): route paperclipai CLI guidance through safe npx form (CWE-78) (#11400)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip provides CLI commands and guidance for operators and
agents
> - The `pnpm paperclipai` script can pass argument values through a
shell
> - Shell re-parsing can execute command substitutions inside quoted
values
> - This pull request routes guidance through inert-argv `npx
paperclipai` commands and adds regression coverage
> - The benefit is safer operator guidance across documentation and
runtime hints

## Linked Issues or Issue Description

This pull request fixes a command-injection-class defect in Paperclip
CLI guidance.

**What happened?**

The `pnpm paperclipai <sub> --flag "$VALUE"` form can re-parse argument
values through a shell. A command substitution inside a quoted value can
execute on the host.

**Expected behavior**

Paperclip guidance must pass CLI values as inert argument values.
Host-derived values must not appear in copyable commands.

**Steps to reproduce**

1. Run a Paperclip guidance command that uses the `pnpm paperclipai`
script.
2. Provide a quoted value that contains a command substitution.
3. Observe that the shell can evaluate the substitution before the CLI
starts.
4. Compare the result with the `npx paperclipai` form.

**Paperclip version or commit**

`5670984b75d109950c968542a0111ebb6967f4da`

**Deployment mode**

All deployment modes that show or use the affected CLI guidance.

**Installation method**

Built from source and installed CLI guidance.

**Agent adapter(s) involved**

Not adapter-specific (core bug).

**Database mode**

Not database-related.

**Access context**

Both.

**Additional context**

The earlier merged PR
[#11343](https://github.com/paperclipai/paperclip/pull/11343) used the
unsafe `pnpm exec paperclipai` form. This fresh PR replaces that
guidance with the safe `npx paperclipai` form.

## What Changed

- Standardize documentation and runtime hints on `npx paperclipai`.
- Remove the broken `pnpm exec paperclipai` guidance.
- Use a static `<host>` placeholder in private-hostname guidance.
- Add regression tests for unsafe forms, continued lines, static hosts,
and offline guidance.

## Verification

- `git diff --check
origin/master...origin/fix/paperclipai-cli-npx-safe-invocation` passes.
- The branch adds `server/src/__tests__/cli-invocation-safety.test.ts`
and updates private-hostname tests.
- CI must run the new tests, typecheck, lint, and build checks.
- Local Vitest execution was not available because this worktree has no
installed Vitest binary.

## Risks

- The change affects operator and agent documentation text.
- The runtime hints now show `<host>` instead of a request-derived host
value.
- No database schema or migration changes exist.
- CI will detect any missed unsafe invocation or type error.

## Model Used

OpenAI GPT-5, exact model ID `gpt-5`, with tool use and code-review
assistance. The model used repository inspection, Git operations, and PR
preparation.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-14 22:11:16 -07:00
Opaque ea3a5ea7d2
fix(recovery): skip successful-run handoff for recovery-action-driven runs (#9010)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The recovery subsystem repairs issues stranded without a valid
disposition: `decideSuccessfulRunHandoff` queues one corrective wake per
successful-but-dispositionless run, and `source_scoped_recovery_action`
wakes a recovery owner for stranded issues
> - `decideSuccessfulRunHandoff` already refuses to treat corrective
handoff runs, issue-monitor runs, and comment-driven wakes as handoff
*sources* — but not runs woken by `source_scoped_recovery_action`
> - Because the handoff idempotency key includes `sourceRunId`, every
succeeding recovery run is a brand-new source: recovery run → handoff
wake → corrective run → new recovery action → recovery run → … with
`DEFAULT_MAX_SUCCESSFUL_RUN_HANDOFF_ATTEMPTS` never binding (it is
per-source-run) and the source-scoped recovery action created with
`maxAttempts: null`
> - The cycle is unbounded, each leg is a ~15s no-op "succeeded" run,
and the designed handoff-exhausted escalation (blocked + exhausted
notice) never engages
> - This PR adds recovery-action-driven runs to the existing skip list,
so recovery runs own their own follow-up path and the stranded-issue
escalation remains the exit when the disposition is still missing
> - The benefit is that missing-disposition recovery converges (one
handoff, then escalation) instead of ping-ponging wake volume
unboundedly

## Linked Issues or Issue Description

Refs #6523 — same wake-loop family (repeated
`source_scoped_recovery_action` wakes); this PR fixes the variant where
the loop partner is the successful-run handoff.

**Observed behavior:** in a 16-agent deployment, one agent produced 223
runs in 2 hours, every run `succeeded` with ~15s duration, with
`contextSnapshot.wakeReason` alternating exactly between
`source_scoped_recovery_action` (109) and
`finish_successful_run_handoff` (108). The source issue never reached
the exhausted escalation.

## What Changed

- `server/src/services/recovery/successful-run-handoff.ts`: new
`isRecoveryActionDrivenRun` predicate (matches
`contextSnapshot.wakeReason === "source_scoped_recovery_action"` or a
present `contextSnapshot.recoveryActionId`), consulted in
`decideSuccessfulRunHandoff` alongside the existing corrective-handoff /
issue-monitor / comment-driven skip guards.
- `server/src/services/recovery/successful-run-handoff.test.ts`: cases
asserting recovery-driven runs are skipped via both markers.

## Verification

- `pnpm -F @paperclipai/server exec vitest run
src/services/recovery/successful-run-handoff.test.ts` → 17 passed (16
existing unchanged + 1 new).
- Production validation (same logic deployed as a dist patch on
2026.626.0): the alternating recovery/handoff wake pattern stopped after
restart; ordinary successful-run handoffs (first corrective wake per
genuine source run) continue to queue.

## Risks

Low-to-moderate, scoped to one decision function. The behavioral shift:
a recovery-action run that succeeds without fixing the disposition no
longer gets a corrective handoff wake — instead the stranded-issue
detector escalates (blocked + recovery owner + exhausted notice), which
per the existing `escalateStrandedAssignedIssue` code is the designed
terminal path. Runs not woken by a recovery action are unaffected
(covered by the existing 16 tests, all green).

## Model Used

Claude Fable 5 (`claude-fable-5`), extended thinking, agentic tool use
via Claude Code. Human-reviewed before submission.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-08-14 23:21:34 -05:00
Dotta 57edb26db4 Merge pull request #11405 from paperclipai/fix/review-policy-verdict-enforcement
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-15 02:06:41 +00:00
Dotta edb8083538 fix(server): serialize interaction review verdicts
Lock the issue before accepting or rejecting review confirmations, reauthorize against the current policy, and cover concurrent policy tightening.

Co-Authored-By: Codex <noreply@openai.com>
2026-08-15 01:53:26 +00:00
Nicky Leach ed8075b535
fix(adapter-utils): order stdin file writes in the sandbox process-session bridge (#11406)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent adapters can run through a sandbox process-session bridge
> - The bridge writes streamed standard input to files before the remote
process reads them
> - Concurrent file writes can make a later chunk visible before an
earlier chunk
> - The remote process can then parse a tail fragment and wait forever
for the missing head
> - This pull request serializes host writes and makes an unexpected
file gap a loud error
> - The benefit is ordered input with a bounded failure path for sandbox
ACP sessions

## Linked Issues or Issue Description

No public GitHub issue exists for this change. The description below
follows `.github/ISSUE_TEMPLATE/bug_report.yml`.

**What happened?**

A sandbox ACP process-session bridge could stall after its handshake
when a run sent a large prompt. The host sent one un-awaited file write
for each standard input chunk. A small later chunk could finish before a
large earlier chunk. The remote poller then sent the tail bytes first.
The agent parser raised an error on the tail fragment, and the head
bytes stayed buffered without a newline.

**Expected behavior**

The bridge must expose standard input files in sequence. The remote
poller must report a clear error when an earlier file remains missing
beyond the retry budget.

**Steps to reproduce**

1. Start an ACP session through a sandbox process-session bridge.
2. Send a prompt that produces multiple standard input file chunks.
3. Delay finalization of an earlier chunk while a later chunk completes.
4. Observe that the remote parser can receive the later chunk first and
the session can stop without a clear error.

**Paperclip version or commit**

The change targets the current `master` branch at the submitted commit.

**Deployment mode**

The bug affects sandbox execution.

**Agent adapter(s) involved**

The failure affects the ACP process-session bridge.

**Database mode**

Not database-related.

**Additional context**

The fix keeps the existing per-file atomic write behavior. It adds
ordering at the host write boundary and a bounded ordering check in the
shared wrapper poll tail.

## What Changed

- Add a per-session promise chain for host standard input file writes.
- Keep a failed write from blocking later chain entries.
- Track the next expected sequence number in the shared wrapper poll
tail.
- Hold later files while an earlier file is missing within the existing
retry budget.
- Emit a loud error and advance after the retry budget expires.
- Add regression tests for host ordering, gap holding, and the loud
error path.

## Verification

- `npx vitest run
packages/adapter-utils/src/execution-target-stdin-race.test.ts` — 9
tests passed.
- `npx vitest run
packages/adapter-utils/src/execution-target-sandbox.test.ts` — 43 tests
passed.
- `pnpm --filter @paperclipai/adapter-utils typecheck` — clean.
- With the source fix reverted, the 3 new tests fail and the 6 original
tests pass.
- CI must pass on the pull request before merge.

## Risks

Low risk.

- The host now serializes writes for each session, which can reduce
write parallelism.
- A failed write still emits one error and destroys the socket, as
before.
- The wrapper can emit a loud error after the existing retry budget when
a file gap persists.
- The change does not alter the atomic per-file write behavior.

## Model Used

OpenAI GPT-5, model ID `gpt-5`, with tool use and code execution. The
context window and internal reasoning details are not disclosed.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-14 18:45:31 -07:00
Dotta 3526b82e2b test(server): expect transactional review transition
Align the watchdog in-review assertion with the atomic update contract.

Co-Authored-By: Codex <noreply@openai.com>
2026-08-15 01:43:27 +00:00
Dotta 277c13529a fix(server): persist review requester atomically
Commit both bound and unbound in-review transition activity in the same transaction as the issue update.

Co-Authored-By: Codex <noreply@openai.com>
2026-08-15 01:38:12 +00:00
Dotta 3a87b143a2 test(server): support locked review policy updates
Keep terminal-update route harnesses aligned with the transactional issue service contract.

Co-Authored-By: Codex <noreply@openai.com>
2026-08-15 01:31:45 +00:00
Nicky Leach 6f26f2a450
fix(adapter-utils): add per-iteration timeout and watchdog to the sandbox callback bridge poll loop (#11341)
## Thinking Path

> - Paperclip runs AI agents through adapters and sandboxed execution
paths
> - The sandbox callback bridge carries file requests between the host
and a sandbox
> - The poll loop waited forever when a sandbox call stopped responding
> - A permanent wait stranded queued requests and hid the run failure
> - This pull request adds bounded timeouts, abort handling, recovery
backstops, and trace reporting
> - The benefit is prompt request failure, safe mutation outcomes,
run-level error reporting, and trace visibility

## Linked Issues or Issue Description

**What happened?**

The sandbox callback bridge could wait forever when a client call
stopped responding without a rejection.

**Expected behavior**

The bridge should fail queued requests and report a run-level error when
the sandbox channel stops responding.

**Steps to reproduce**

1. Start a sandbox callback bridge.
2. Queue a request.
3. Make the sandbox call stop responding.
4. Observe that the request does not receive a failure response.

**Paperclip version or commit**

Commit `edc4f71b460c600f97cf44cb486d5cac72ca2db9`.

**Deployment mode**

Built from source.

**Installation method**

Built from source with pnpm.

**Agent adapter(s) involved**

Custom or external sandbox callback bridge.

**Database mode**

Not database-related.

## What Changed

- Add a per-iteration timeout for `listJsonFiles` and
`processRequestFile`.
- Add a watchdog that fails pending requests when the loop makes no
progress.
- Abort a hung handler and use a non-retryable 504 backstop when its
outcome can be indeterminate.
- Retry recovery writes and keep queued requests when a recovery write
fails.
- Forward the indeterminate-outcome header through the execution target.
- Record worker failures through the
`sandbox.callbackBridge.workerFailed` trace span.
- Add tests for timeout, watchdog, recovery, mutation safety, header
forwarding, and fast-request behavior.

## Verification

- Run `pnpm exec vitest run
packages/adapter-utils/src/sandbox-callback-bridge.test.ts
packages/adapter-utils/src/execution-target-sandbox.test.ts`.
- Confirm that the PR test, typecheck, build, end-to-end, serialized
test, and security checks pass.
- Confirm that the current PR head is
`edc4f71b460c600f97cf44cb486d5cac72ca2db9`.
- Confirm that the PR changes four files: the callback bridge, its
tests, the execution target, and its tests.

## Risks

The default timeout can fail a slow but valid sandbox call. The defaults
remain configurable, and the iteration timeout stays below the sandbox
response deadline. A mutation that may have committed returns a
non-retryable 504 outcome so the caller does not apply it twice.

## Model Used

OpenAI Codex, GPT-5 current runtime, with extended reasoning and tool
use. The exact context window is 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 checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-14 18:26:52 -07:00
Dotta 991f40bb2e fix(server): serialize review policy verdict authorization
Recheck terminal verdict and policy mutations under a row lock, and scope interaction verdict enforcement to the review confirmation itself.

Co-Authored-By: Codex <noreply@openai.com>
2026-08-15 01:25:17 +00:00
Dotta 373b675f94 fix(server): prevent review policy verdict downgrade bypass
Authorize verdicts and policy changes against the stored restrictive review policy, remove downgrade guidance, and cover both restrictive policies with route regressions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-15 01:12:34 +00:00
Dotta 37fde84abd fix(server): enforce review policy on interaction verdicts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-15 01:12:24 +00:00
Dotta 8ee1fb21a6
feat(ui): badge the review policy when it constrains approval (#10938)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents move their work into review, and a reviewer must then give a
verdict on it
> - By default anyone with write access can give that verdict, including
the agent that did the work
> - The server can constrain that default per issue with a
`reviewPolicy` column, but no screen showed the value
> - A reviewer could therefore press Approve on a review that the server
refuses, and get a 403
> - This pull request shows the policy as a badge on the two surfaces
where a person gives a verdict
> - It also makes an agent verdict read as a verdict in the activity
timeline
> - The benefit is that a reviewer sees who can approve before they try

## Linked Issues or Issue Description

No public GitHub issue exists for this change. The description below
follows
`.github/ISSUE_TEMPLATE/enhancement.yml`.

**What existing behavior does this improve?**

The issue review flow. A reviewer cannot see the approval constraint on
an issue
before they give a verdict.

**Subsystem affected**

Web UI (`ui/`), with one supporting change in the server attention
service.

**Current behavior**

The server stores an optional approval constraint for each issue in a
`reviewPolicy` column. The column has three meaningful states: the
default
(`NULL` or `anyone`), `not_creator`, and `human_only`. The server
enforces the
constraint when it receives a verdict.

No screen shows the value. Two problems follow:

1. A reviewer presses Approve on a review that the server refuses. The
server
   answers 403, and the reason is not visible on the card.
2. An agent that accepts or rejects a review renders in the activity
timeline as
the raw action id, for example "issue thread interaction accepted". A
person
   who reads the timeline cannot tell that a verdict was given.

**Proposed behavior**

Show the constraint as a read-only badge on the two surfaces where a
person
gives a verdict. Show no pixels for the default state, because the
default is
what every issue already does. Make an agent verdict read as a verdict
in the
timeline.

Only agents set the column today, so this change adds no control to set
it.

**Reason and benefit**

A reviewer sees the constraint before they act. This prevents the 403,
and it
removes the need to explain the 403 afterwards. The timeline also
becomes
complete, because it now shows agent verdicts and human verdicts in the
same way.

**Breaking changes**

None. The change adds a badge and changes copy. It adds no column, no
endpoint,
and no request.

**Additional context**

The server-side column and the verdict enforcement landed earlier in
#10931.
This pull request is the user interface for that column. The default
state stays
unchanged on screen, so the badge appears on a small number of issues.

## What Changed

- **A read-only "Approvals" row** in the issue Execution properties. The
row
renders *only* for a constrained policy: "Anyone else" (`not_creator`)
or
"Human only" (`human_only`). A `NULL` or `anyone` column adds no row, so
the
  panel is untouched on the overwhelming majority of issues.
- **The same badge on the stalled-review card** in `/decisions`, above
the three
review verbs. A reviewer now sees the constraint before they press
Approve.
  The condition is the same, so the default card is unchanged.
- **Agent verdicts read as verdicts in the activity timeline.** An agent
that
accepted or rejected a review request previously rendered the raw action
id
("issue thread interaction accepted"). It now reads "approved the
request". A
  stalled-review decision names the verb that the actor chose.
- **A cleared policy reports as "anyone", not "none",** in the
field-change
receipt. The `reviewPolicy` column is nullable by default, so an absent
value
  is a real setting rather than a missing one.
- **All copy comes from `ui/src/lib/review-policy.ts`.** Its badge
lookup returns
`null` for the default. This makes "no pixels for the default" one
enforced
decision instead of a condition repeated at each call site. It also
keeps the
  badge, the activity line, and the receipt reading alike.
- **The server attention service carries the policy** on the review
attention
  subject, so the stalled-review card can read it.

## Verification

Automated tests:

- `ui/src/lib/review-policy.test.ts` — the default returns no badge,
however the
column spells it (`null`, `undefined`, `"anyone"`). An unrecognised
policy from
  the wire shows nothing rather than leaking an enum value.
- `ui/src/components/AttentionQueueRow.test.tsx` — no badge on the
default card,
and the verbs still render. Suppression of the badge must not suppress
the card.
- `ui/src/components/IssueProperties.test.tsx` — no Approvals row on the
default.
  The constrained row contains no `button`, so nothing there can PATCH.
- `server/src/__tests__/attention-service.test.ts` — the review
attention subject
carries the policy, and subjects built from narrower selects do not
claim one.

Run them with:

```sh
pnpm vitest run ui/src/lib/review-policy.test.ts \
  ui/src/components/AttentionQueueRow.test.tsx \
  ui/src/components/IssueProperties.test.tsx \
  server/src/__tests__/attention-service.test.ts
```

Manual steps:

1. Open an issue that has no `reviewPolicy`. Confirm that the Execution
   properties panel shows no Approvals row.
2. Set the column to `not_creator`. Reload the issue. Confirm that the
Approvals
   row reads "Anyone else", and that the row has no control.
3. Move that issue into review. Open `/decisions`. Confirm that the
stalled
   review card shows the same badge above the review verbs.
4. Let an agent approve the review. Confirm that the activity timeline
reads
   "approved the request" and not "issue thread interaction accepted".

Screenshots were captured at 1440x900 and 390x844, in light mode and
dark mode,
with the three policy states side by side. The leftmost column in each
capture is
the default. It carries no badge and no extra row.

## Risks

Low risk.

- The change is additive on screen. Every new surface is behind a
constrained
  policy, so the default path renders exactly as before.
- The badge is read-only. It has no control and sends no request, and a
test
  asserts that the row contains no `button`.
- An unknown policy value from the wire renders nothing. It does not
render the
  raw enum.
- No migration, no schema change, and no endpoint change.

## Model Used

Claude Opus 5 (Anthropic), model id `claude-opus-5[1m]`, 1M context
window,
with extended thinking and tool use enabled. Used through Claude Code
for the
implementation, the tests, and this description.

## Checklist

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 20:29:36 -04:00
Nicky Leach 69027cbaae
fix(workspaces): reopen archived git worktree for managed_checkout projects (#11395)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Execution workspaces give agent tasks isolated Git worktrees
> - Archived isolated workspaces must reopen against a live project
checkout
> - A managed_checkout project has no project workspace directory in its
row
> - The reopen path used the removed archived worktree as the Git
working directory
> - This pull request resolves the live managed checkout and reports a
clear error when it is unavailable
> - The benefit is reliable workspace reopen behavior after archive
cleanup

## Linked Issues or Issue Description

Related public pull request:
[#6164](https://github.com/paperclipai/paperclip/pull/6164) clears
archive state during un-archive. This pull request fixes the separate
reopen failure that occurs after archive cleanup.

**What happened?**

An archived isolated `git_worktree` workspace under a `managed_checkout`
project failed to reopen after cleanup. The route attempted to run Git
in the removed archived worktree and returned a generic service error.

**Expected behavior**

The reopen path should use the live managed checkout as the Git base
directory and should return a clear error when that directory is
unavailable.

**Steps to reproduce**

1. Create a project with `managed_checkout` source control.
2. Create and archive an isolated `git_worktree` execution workspace.
3. Let archive cleanup remove the worktree.
4. Reopen the workspace for an issue.

**Paperclip version or commit**

`cab0c31dc61310106caef42ca244e9f7b0f19460`

**Deployment mode**

Local dev with the default embedded database.

**Agent adapter(s) involved**

Not adapter-specific. This issue affects core workspace handling.

## What Changed

- Resolve the live managed checkout when a managed project reopens an
archived Git worktree.
- Keep local-folder projects on their project workspace directory.
- Validate the Git base directory before `git rev-parse` and return a
scrubbed error.
- Add nine regression tests for workspace reopen behavior.

## Verification

- `server` TypeScript check passes with `tsc --noEmit`.
- `server/src/__tests__/execution-workspace-reopen.test.ts` passes with
9 tests.
- GitHub Actions must pass all required PR checks.

## Risks

Low risk. The change affects only archived isolated workspace reopen
behavior. It reuses the existing managed checkout and Git authentication
helpers. It adds no new credential path, endpoint, or telemetry.

## Model Used

OpenAI GPT-5 assisted with review and GitHub operations. The
implementation author supplied the code and test results.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-14 16:44:31 -07:00
Tonio 35a9b98733
fix(ui): theme the onboarding wizard decorative panel instead of hardcoding dark (#11379)
The onboarding wizard's decorative right-hand panel, which holds the ASCII
paperclip illustration, hardcoded a near-black surface. `ThemeContext`
supports light and dark and follows `prefers-color-scheme`, so in light mode
a new customer met the product as a pale form beside a solid black rectangle
— on the one screen meant to introduce it. The glyphs inside already used
`text-muted-foreground`, so the panel was the only part ignoring the theme.

It now uses the paired `bg-muted` surface, which is defined in both themes
(`oklch(0.97 0 0)` light, `oklch(0.269 0 0)` dark), so the illustration reads
as ink on a surface either way and follows any future theme without another
fix.

The guard for it asserts the complete set of `bg-` classes on the panel is
`["bg-muted"]`, anchored to the `<AsciiArtAnimation />` wrapper rather than
scanning the file. Forbidding specific spellings is what failed here
originally: the first version checked `bg-[#rrggbb]` and silently stopped
guarding anything once master migrated the class to `bg-(--hex-1d1d1d)`.
Naming what is allowed cannot decay that way, and it catches named colours
like `bg-black` that no spelling list covered.

Lands the work from #8982 by @stubbi, whose two commits are included
unchanged with their authorship. The rebase and the guard are mine.

Tested: ui typecheck clean; both theme cases fail against four spellings of
the regression — `bg-(--hex-1d1d1d)`, `bg-[#1d1d1d]`, `bg-black`,
`bg-zinc-900` — where the original caught only one and my first widening
caught two. Full ui suite 3958 pass, with one timezone-dependent
IssueProperties failure present on master. All CI gates green; Greptile 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:06:20 -07:00
LeeJ a53cc8819b
fix(claude-local): pipe print prompt via stdin (#9500)
Fixes #2444.
Refs #4947.

The `claude_local` adapter launched Claude Code as
`claude --print - --output-format stream-json --verbose`. Paperclip writes
the rendered task prompt to Claude's stdin, but current Claude Code releases
can treat the stale `-` positional marker as the prompt itself, so Claude
received the literal string `"-"` instead of the issue body. The customer's
task ran against no content at all.

The fix keeps `--print` mode and stdin delivery, and removes the stale `-`.

Adds regression coverage on both sides of the delivery path: a `claude_local`
assertion that `--print` is present, `"-"` is absent and the prompt still
reaches stdin, and an adapter-utils case proving the sandbox run-log command
wrapper preserves stdin while streaming logs.

Authored by @elJayAdvisor, whose commit is included unchanged with their
authorship. The branch had gone stale and was showing CONFLICTING; the
conflict was in `execution-target-sandbox.test.ts`, where their new test was
added at the same point as master's `creates the process session directories
only in the launch exec` case and git interleaved the two into one hunk.
Resolved by taking master's file and re-inserting their test whole, after
checking every helper it needs still exists there.

Verified: the bug was still live on master at `execute.ts:838`; the
regression test genuinely catches it — restoring the stale `-` fails
`expect(captured.argv).not.toContain("-")`; `@paperclipai/adapter-claude-local`
and `@paperclipai/adapter-utils` typecheck clean; 67 pass across the two test
files. All CI gates green; Greptile 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:34:15 -07:00
Dotta 8cb0ce0de5
fix(ui): restore queued message interrupt action (#11374)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The task thread lets an operator add guidance while an agent run is
active
> - A new message can wait behind that active run as a queued message
> - The classic task view lets the operator interrupt the target run
from that queued message
> - The redesigned task view did not expose the same action
> - This pull request restores the action and keeps it bound to the
exact target run
> - The benefit is that operators can apply urgent guidance without
switching task views

## Linked Issues or Issue Description

**What happened?**

The redesigned task view showed `Queued` for a queued operator message,
but it did not show the existing interrupt action.

**Expected behavior**

The queued message must show `Interrupt` next to `Queued`. The action
must stop the exact run that the message is waiting behind.

**Steps to reproduce**

1. Open a task in the redesigned task view while an agent run is active.
2. Send a new operator message so it enters the queued state.
3. Observe that the queued message has no interrupt action.

**Paperclip version or commit**

`bc0b5a1642`

**Deployment mode**

All deployment modes that use the redesigned task view.

## What Changed

- Preserve persisted queued state and the target run ID in the
redesigned thread model.
- Render a token-compliant `Interrupt` action beside the queued state.
- Reuse the existing exact-run interrupt callback and show a disabled
`Interrupting…` state during the request.
- Keep an assigned queue target immutable so an in-flight comment cannot
rebind its interrupt action to a replacement run.
- Add regression tests for persisted queued messages, replacement-run
races, and the in-progress action state.
- No documentation update was required because this restores existing
behavior.

## Verification

- `pnpm exec vitest run ui/src/components/TaskChatThread.test.tsx`
- `pnpm exec vitest run ui/src/components/TaskChatThread.test.tsx
ui/src/components/task-chat/task-chat-adapter.test.ts
ui/src/pages/IssueDetail.test.tsx -t 'queued message actions|queues
messages against a queued live run and interrupts that exact
run|commentsToTaskChatItems'`
- `pnpm exec vitest run ui/src/pages/IssueDetail.test.tsx -t 'queued
message|queues messages'`
- `pnpm exec vitest run ui/src/pages/IssueDetail.test.tsx` (52 passed)
- `pnpm -r typecheck`
- `pnpm test:run` (all server and UI groups passed; the CLI group passed
after inherited static AWS credential variables were omitted)
- `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY pnpm exec vitest
run cli/src/__tests__/secrets.test.ts`
- `pnpm build`
- `pnpm check:token-gates`

## Risks

Low risk. The change only adds an action to queued messages that have a
target run and an interrupt callback. Messages without both values keep
the current rendering.

> 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 with GPT-5. The runtime does not expose a more specific
deployment ID or context-window size. The model used reasoning,
repository tools, code execution, 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 checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-14 14:26:18 -04:00
Tonio 66515582e4
fix(onboarding): do not restore wizard state for a company the user does not own (#11370)
Onboarding persists a draft to `localStorage`, including `createdCompanyId`.
That key is scoped per browser origin, not per account, so a browser that has
already run onboarding hands the stored company id to the next session on the
same origin — whoever is signed in. Every downstream call then targets a
company that account may not own: goals, agents and issues are created there,
and requests fail with authorization errors.

`restoreOnboardingState` now returns a saved draft only when the signed-in
account owns the company it names. Otherwise the draft is discarded and the
stale blob removed.

The wizard splits into a gate and an inner component because the inner one
has ~20 `useState(saved?.x ?? default)` initializers, and an initializer runs
only on the first render. Mounting before the restored draft is final locks
every field to its default with no way back, so the gate waits for the
company list while it is loading.

Ownership is judged only against a list that actually answered. Any company
query error makes it undecidable, whatever the list contains — the companies
cache is not account-scoped and survives sign-out, so a failed refetch after
an account switch can leave the previous account's companies in hand, and
trusting a non-empty list there would hand one account's draft to the next.
Nothing is restored and nothing is deleted in that state; the next successful
load decides.

Judging the draft and mounting the wizard are separate questions. The gate
withholds the wizard only while the list is *loading*, never on error: the
companies query sets `retry: false`, and with no companies the dashboard
offers a "Get Started" button wired to onboarding, so blocking there would
make that button do nothing at all. Mounting costs the draft nothing, because
the persist effect is itself gated on the wizard being open.

All four draft-storage call sites — read, write, cleanup, reset — go through
one guarded helper. Storage access throws outright where a browser denies it,
and each site sits in a render, an effect or a close handler, so an escaping
exception took down something the customer was using.

Lands the work from #9900 by @stubbi, whose two commits are included
unchanged with their authorship. The rebase, the error-path handling and the
storage guards are mine.

Follow-up filed separately: sign-out should remove account-scoped cached data
rather than invalidating two keys. This change is defensive and protects
onboarding only.

Tested: ui typecheck clean; 73 pass across the seven onboarding suites; full
ui suite 3952 pass, with one timezone-dependent IssueProperties failure
present on master in a file this does not touch. All CI gates green;
Greptile 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:08:48 -07:00
Tonio bc0b5a1642
fix(ui): onboarding wizard keeps an invisible disabled adapter selected (#11371)
The wizard defaults `adapterType` to `claude_local`, and a saved draft can
name any adapter. The grid only renders adapters the server has enabled, so
on an instance where the held adapter is disabled nothing appears selected
while the wizard still holds it — and the first agent is hired on an adapter
the deployer turned off, which can never acquire a lease.

The selection now snaps to the first enabled, non-coming-soon adapter
whenever the held one is not visible, and adapter-specific model defaults
follow it.

The snap waits for the adapter registry to load. External adapter types are
registered into the UI registry only once the adapters query resolves, so
before that a saved external adapter is indistinguishable from a disabled
one — snapping on that transient list would replace the customer's choice
with a built-in and the persist effect would write it down. This gate fails
closed, unlike the fail-open gates in onboarding, because the directions of
harm are opposite: acting early silently rewrites a saved answer, while
waiting merely leaves the selection alone, which is the behaviour that
existed before the snap did.

The test file is named `OnboardingWizard.adapters.test.tsx` rather than
`OnboardingWizard.test.tsx`, which is the name #11370 uses for its restore
gate. Both merged cleanly onto master alone but collided with each other on
add/add, and nothing in either status showed it.

Lands the work from #9900's sibling, #9501, by @stubbi, whose commit is
included unchanged with their authorship. The rename and the registry gate
are mine.

Tested: ui typecheck clean; 51 pass across the adapter, hook, dialog,
config-form and wizard-step suites, including the other callers of the
adapter hook since that module changed. All CI gates green; Greptile 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 10:27:59 -07:00
Dotta d2665ff6b4
fix(ui): align the mobile task chat composer with the thread (#11296)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The task thread is where people read work and guide agents.
> - The mobile composer should use the same content width as the thread.
> - The composer kept the desktop 80% width on mobile, so its edges did
not align with the thread.
> - Long assignee-aware placeholder text could also clip inside the
mobile editor.
> - Some extracted style tokens used legacy HSL wrappers around complete
semantic colors, which made those declarations invalid.
> - This pull request makes the composer full width on mobile, preserves
the narrower desktop layout, wraps the placeholder, and repairs the
invalid color compositions.
> - The benefit is a stable mobile composer that aligns with the task
thread and keeps its intended visual styles.

## Linked Issues or Issue Description

Related work: Refs #11263.

**What happened?**

At mobile widths, the task chat composer used the same 80% width as the
desktop composer. Its horizontal edges did not align with the full task
thread. A long assignee-aware placeholder could clip on one line. The
composer's extracted shadow also used a legacy `hsl(var(...))` wrapper
around complete semantic color values, so the browser could reject the
declaration.

**Expected behavior**

The composer must match the task thread width on mobile. It must stay
narrower on larger screens. Long placeholder text must wrap inside the
editor. Semantic color tokens must form valid shadows and gradients.

**Steps to reproduce**

1. Open a task with the chat-style thread on a mobile viewport.
2. Compare the composer edges with the task thread edges.
3. Select an assignee whose placeholder text wraps to two lines.
4. Inspect the computed composer shadow and the extracted semantic color
styles.

**Paperclip version or commit**

The change is based on `dc6fcd1ff1` from `master`.

**Deployment mode**

Local build from source. The behavior also applies to packaged web
builds.

## What Changed

- Made the task chat composer full width below the medium breakpoint and
kept the 80% desktop width.
- Matched the composer dock padding to the task thread padding.
- Allowed long composer placeholders to wrap and reserved enough mobile
editor height for two lines.
- Replaced invalid legacy HSL wrappers around full semantic colors in
extracted shadows, gradients, and approval styles.
- Added a token gate that prevents legacy `hsl(var(--token))` wrappers
from returning.
- Added focused regression tests for responsive width, padding,
placeholder wrapping, mobile height, and semantic shadow validity.

## Verification

- `pnpm check:token-gates` — all four gates pass.
- `pnpm --filter @paperclipai/ui exec vitest run
src/components/TaskChatThread.test.tsx
src/components/task-chat/TaskChatComposer.test.tsx
src/components/task-chat/TaskChatComposerStyles.test.ts` — 37 tests
pass.
- `pnpm --filter @paperclipai/ui typecheck` — passes.
- `pnpm --filter @paperclipai/ui build` — passes. The build prints
existing CSS optimizer and bundle-size warnings.

## Risks

- Low risk. The width change is limited to the mobile breakpoint. The
desktop 80% layout remains in place.
- The semantic token fixes can affect shadows and gradients that were
previously invalid. The new gate prevents the invalid wrapper pattern
from returning.

> 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 with `gpt-5.6-sol`. The context-window size is not
exposed in this environment. The model used reasoning, repository tools,
code execution, and GitHub tools.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-14 12:53:11 -04:00
Tonio d95340b0b8
feat(ui): send a company with no agent into onboarding, at the right step (#11352)
A company with no agent cannot do anything: no runs, no tasks, nothing to
show. The dashboard says so in a banner with a link, which asks the customer
to notice a problem the product can fix for them. It is worse for a company
created by Paperclip Cloud: Cloud creates the company before the tenant
boots, so the companyless redirect never runs, and the customer arrives on an
empty dashboard straight out of a signup flow that already asked for a
mission.

The dashboard now opens onboarding when the agent list has loaded and is
empty, and onboarding opens on the agent step when the company already has
its mission — read from the company-level goal the seed writes, under the
query key the launch path already uses, so it shares a cache entry rather
than adding a fetch.

The step is decided once. `initialStep` is derived from the company list and
the goal list, so it changes on any retry, refetch or cache invalidation. An
effect that took it as a dependency called `setStep` on every one of those
and moved a customer who was already mid-flow. Gating the input only narrowed
that window; it could not close it. The step now belongs to the request that
opened the wizard: the effect reads it through a ref and is keyed on the
wizard opening or the company changing. `createdCompanyIdRef` beside it
already used this pattern for the same reason.

That exposed a path nothing had ever taken. A company reached the mission
step only by creating itself on step 1, so opening an existing company there
found code that had never run: `companyName` is only typed on step 1, and
both ways forward require it, so the step could not be completed at all; and
confirming advanced without writing anything, so the mission the customer
typed was discarded. Both fixed, and the write now reconciles against the
goal list rather than adding a second company goal, since the mission lookup
fails open and can send a company that has one back to that step.

Company-scoped state now stays with its company. `clearCompanyScopedState`
runs when the route replaces a company and when it withdraws one — the same
event, and clearing half of it left a goal id that made the next company skip
a mission it had never given. `stillTheSameCompany` guards all five async
writes, after the server work rather than before it, so a company switch
mid-flight cannot hand the new company the old one's goal, project, issue or
agent, and cannot leave a hired agent without its instructions file. The
keyboard path honours `loading` like every button already did.
`claimOnboardingOffer` makes onboarding an offer that stays declined for the
visit.

Route ownership is now recorded whenever the route names a company, including
one the wizard already holds. This changes a documented rule deliberately:
without it a self-created company was never withdrawn, so `/onboarding` would
show "create a company" while still holding the previous one and write the
customer's new mission into it.

Tested at the seam, because every defect on this branch lived between a value
and its consumer and the predicate tests passed at every stage.
`OnboardingWizard.step.test.tsx` renders the real wizard against the real
resolver and the real mission hook across 18 cases, and each was
fault-injected against the code it replaces rather than trusted on a green
run. That caught a case that passed against the broken code, and a race in
one of the guards.

ui typecheck clean; full ui suite 3923 pass, with one timezone-dependent
IssueProperties failure present on this branch's base in a file this change
does not touch. All CI gates green; Greptile 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 08:18:07 -07:00
Dotta dc6fcd1ff1
fix(ui): move agent secret access to searchable secrets tab (#11283)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The agent configuration UI controls each agent and its allowed
secrets.
> - The environment variable editor already has a secret selector with
search and folder navigation.
> - The secret access editor used a basic list and made large secret
stores hard to use.
> - The secret access controls also occupied the main Configuration tab.
> - This pull request reuses the rich selector and moves secret access
to a dedicated Secrets tab.
> - The benefit is one consistent secret selection workflow with clearer
agent configuration navigation.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The agent detail configuration view and its secret access editor.

**Subsystem affected**

`ui/` — React and Vite board UI.

**Current behavior**

The secret access editor uses a basic select control. It does not
provide the search and folder navigation available in the environment
variable editor. The editor also appears inside the Configuration tab.

**Proposed behavior**

The secret access editor uses the shared secret picker. Users can search
secrets and browse slash-delimited folders. Agent details provide a
dedicated Secrets tab for this editor.

**Reason and benefit**

Large secret stores are slow to scan in a flat list. Reusing one
selector reduces UI differences and makes scoped secret access easier to
manage.

**Breaking changes**

None. The API and saved secret access data do not change.

## What Changed

- Reused the environment variable secret picker in the agent secret
access editor.
- Preserved secret version selection and the create-secret action,
including nested-popover focus handling.
- Added a route-backed Secrets tab to agent details and removed secret
access controls from Configuration.
- Guarded unsaved configuration across tab, link, browser-history, and
action-triggered navigation.
- Rechecked dirty state when navigation-producing agent actions finish,
covering edits made while a request is pending.
- Added component, page, and Storybook coverage for the workflow.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/components/AgentActionButtons.test.tsx
src/components/AgentConfigForm.render.test.tsx
src/components/AgentSecretAccessEditor.test.tsx
src/components/environment-variables-editor/EnvironmentVariablesEditor.test.tsx
src/pages/AgentDetail.progress.test.ts` — 82 tests passed.
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm check:token-gates`
- All GitHub PR checks passed on `5209c5b787`, including build, canary,
general and serialized tests, and all three e2e shards.
- Greptile completed at 5/5 with zero unresolved review threads.

## Risks

- Low risk. The API and persisted binding format are unchanged; this
changes agent configuration navigation and secret selection UI.
- Dirty-state guards now cover direct navigation, Back/Forward history,
and navigation-producing agent actions, including pending-request races.
- Tests cover tab separation, secret access updates, search, folder
navigation, focus restoration, and navigation rejection.
- No documentation change is required because commands, contracts, and
setup steps do not change.

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-14 08:03:20 -04:00
Tonio aac6ce82e1
fix(ui): read the onboarding company prefix from the path, not the route match (#11351)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - New users meet the product through an onboarding wizard that creates
their company, their first agent, and a starter task
> - The wizard also serves an existing company, at
`/{PREFIX}/onboarding`, to add another agent to it
> - On that route the wizard ignores the company in the URL and opens at
"create a company" instead
> - It reads the prefix with `useParams()`, but it renders beside
`<Routes>` rather than inside it, so there is no route match to read
> - This pull request reads the prefix from the pathname, which is
available without a match
> - The benefit is that the URL a user follows decides what the wizard
asks them

## Linked Issues or Issue Description

No public issue exists for this. The problem follows.

**What happened?**

Open `/{PREFIX}/onboarding` for a company that already exists. The
wizard opens at step 1 and asks the user to create a company. The
company named in the URL is ignored.

**Expected behavior**

The wizard recognises the company in the URL and opens at step 2, so the
user adds an agent to that company instead of creating a second one.

**Steps to reproduce**

1. Create a company, so it has an issue prefix.
2. Go to `/{PREFIX}/onboarding`.
3. Read the first screen. It asks for a company name.

**Paperclip version or commit**

`master` at `5ca7b4c1f`.

**Deployment mode**

Any. This is client-side routing and does not depend on the server.

## What Changed

- `ui/src/lib/onboarding-route.ts` — adds
`companyPrefixFromOnboardingPath()`, which reads the prefix from the
pathname.
- `ui/src/components/OnboardingWizard.tsx` — uses that value when the
route match supplies none. One line, plus the import.
- `ui/src/lib/onboarding-route.test.ts` — six cases for the new
function.

`OnboardingWizard` renders beside `<Routes>` in `App.tsx`, so
`useParams()` returns nothing and `companyPrefix` was always
`undefined`. `resolveRouteOnboardingOptions` then took its no-prefix
branch every time. `useLocation()` needs only the router, not a match,
and the wizard already calls it.

The route match is still read first. If the wizard later moves inside
the route tree, this code does not need to change.

The new parser accepts the same shape as `isOnboardingPath()`: the
prefix is the first of exactly two segments. One test asserts the two
agree, because a disagreement would either open the wizard where no
company resolves, or resolve a company where onboarding is not served.

### Why the change is this small

Three pull requests are open against `OnboardingWizard.tsx` — #9900,
#9501 and #8982. A larger change there would collide with all three.
Almost all of this lands in `onboarding-route.ts`, a small file of pure
functions with existing tests.

## Verification

- `npx tsc --noEmit -p ui/tsconfig.json` — clean.
- `npx vitest run ui/src/lib/onboarding-route.test.ts` — 18 pass.
- `npx vitest run ui/src` — 3883 pass, 445 files.

One test shows the defect and the fix together. With `companyPrefix:
undefined`, which is what the wizard supplied before,
`resolveRouteOnboardingOptions` returns `{ initialStep: 1 }`. With the
parsed prefix it returns `{ initialStep: 2, companyId: "c1" }`.

**Pre-existing failures, unrelated:** `IssueProperties.test.tsx` and
`StatusCards/format.test.ts` fail on clean `origin/master` with these
changes stashed. Both look date-dependent.

**Not done:** no manual browser check. The behaviour is covered by unit
tests at the function boundary, and the wizard's own suite passes.

## Risks

Low. The route match is still preferred, so behaviour changes only where
`useParams()` gave nothing — which today is every render of this
component.

The parser returns a prefix only for a two-segment path ending in
`onboarding`, so no other route can start matching. An unknown prefix
already falls back to step 1 in `resolveRouteOnboardingOptions`, and
that path is unchanged.

To revert, remove the fallback in the wizard. The new function has no
other caller.

## Model Used

Claude Opus 5 (`claude-opus-5`), through Claude Code. Extended thinking
enabled. Tool use enabled: file read and edit, shell command execution
for typecheck and the test runs, and the 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 checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-13 21:45:39 -07:00
Nicky Leach 5ca7b4c1fe
fix(security): standardize paperclipai CLI guidance on safe npx path (#11343)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip provides CLI guidance to agents and operators through
documentation and runtime messages.
> - Content-bearing `pnpm paperclipai` examples send arguments through a
shell.
> - Shell evaluation can execute command substitutions in untrusted
argument content.
> - Runtime hostname guidance can also place request-derived content
inside a shell command.
> - This pull request uses `npx paperclipai` for content-bearing
guidance and uses a static hostname placeholder.
> - The benefit is safer copy-paste guidance for agents and operators.

## Linked Issues or Issue Description

**Issue type**
Incorrect information

**Where is the issue?**
CLI guidance in `doc/CLI.md`, `skills/paperclip/SKILL.md`,
documentation, and runtime-generated hints.

**What's wrong?**
Content-bearing `pnpm paperclipai` commands can pass argument text
through `/bin/sh`. Shell command substitution in an argument can execute
before the CLI receives the value.

**Suggested fix**
Use `npx paperclipai` for content-bearing commands. Use a static
`<host>` placeholder when runtime guidance displays the allowed-hostname
command.

## What Changed

- Replace content-bearing `pnpm paperclipai` examples with `npx
paperclipai` across the documentation and agent-facing guidance.
- Update runtime-generated CLI hints to use a static `<host>`
placeholder.
- Add safety notes to `doc/CLI.md` and `skills/paperclip/SKILL.md`.
- Add scans and regression tests for unsafe invocation and hostile
hostname headers.
- Keep fixed lifecycle commands and `pnpm --filter @paperclipai/*` build
commands unchanged.

## Verification

- Run `tsc --noEmit` for the changed server files.
- Run `cli-invocation-safety.test.ts`.
- Run `private-hostname-guard.test.ts`.
- Confirm that hostile hostname headers do not enter shown shell
commands.
- Confirm that the three commits contain the required Paperclip
co-author trailer.

## Risks

- This change updates documentation and diagnostic text across many
surfaces.
- Fixed lifecycle and setup commands remain unchanged.
- The tests fail if content-bearing `pnpm paperclipai` guidance returns.
- The change does not alter the CLI argument parser.

## Model Used

OpenAI Codex, GPT-5, tool use, code execution, and repository review
assistance.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-13 16:43:21 -07:00
Nicky Leach 05d58cd884
fix(tool-gateway): keep unsigned ask-first requests out of the review queue without cancelling them (#11338)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The tool gateway creates approval requests and the review queue
reads them
> - The gateway creates a request row before it adds the signature
> - A review-queue read can see the row during that short unsigned state
> - The old read path cancels the unsigned row, so approval returns `409
action_not_pending`
> - This pull request hides unsigned in-flight rows and keeps them
pending until signing finishes
> - The benefit is that approval succeeds while invalid signed requests
remain cancelled

## Linked Issues or Issue Description

**What happened?**

A review-queue read cancelled a pending tool action request when the
request had no signature yet. The next approval call returned `409
action_not_pending`.

**Expected behavior**

The review queue must hide an unsigned in-flight request and keep its
state as `pending`. A request with an invalid signature must remain
cancelled.

**Steps to reproduce**

1. Create a require-approval tool action request.
2. Read the review queue while the request signature is still null.
3. Approve the request after the creator adds the signature.
4. Observe that the old code cancels the request and the approval call
fails.

**Paperclip version or commit**

Commit `720aa0a494bbaa1711bc7a3d795f810765915bfe`.

**Deployment mode**

Local dev with the embedded PGlite database.

**Installation method**

Built from source with pnpm.

**Agent adapter(s) involved**

Not adapter-specific. This is a core tool access service bug.

**Database mode**

Embedded PGlite.

**Access context**

Board and agent tool approval flow.

## What Changed

- Keep a pending request with a null signature out of
`listActionRequests` results.
- Cancel a request when its non-null signature fails verification.
- Add a permanent regression test for the unsigned request transition.
- Update the contract test for unsigned and invalid-signature requests.

## Verification

- Run the tool access service, tool gateway service, tool gateway, and
tool access policy service tests.
- Confirm 227 tests pass.
- Run the `@mcp-runnable` Playwright end-to-end suite in CI.
- Run the US-9 loop 30 times in CI.

## Risks

The change alters review-queue filtering for unsigned requests. A null
signature now means that signing remains in progress. Invalid signed
requests keep the existing cancellation behavior. The change has no
database migration.

## Model Used

OpenAI Codex, GPT-5, with tool use and code execution. The model
reviewed the handoff, repository rules, and pull request state. The
implementation author supplied the code and 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
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-13 16:15:12 -07:00
Austin 0819cac4c6
feat(secrets): add agent-readable /secrets/catalog endpoint (#9530)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can be configured with env bindings that reference company
secrets — they specify which secret by UUID in `adapterConfig.env`
> - But there is no API endpoint agents can call to look up a secret
UUID by name — `GET /companies/:companyId/secrets` is board-only, and
the internal `secrets.resolve` handler only accepts UUIDs
> - So when an agent needs to wire a new secret (e.g. an API key for a
new skill), it has no way to discover the UUID from a known name like
`HOMEBOX_API_KEY` — the user must find it by inspecting browser network
traffic
> - The fix is a read-only catalog endpoint that agents can call to get
the `id`/`name`/`key`/`status` mapping — no values, no provider config —
just enough to resolve a name to a UUID
> - This PR adds `GET /companies/:companyId/secrets/catalog`, guarded by
`assertBoardOrAgent` + `assertCompanyAccess`, so agents can discover the
UUID they need without board-level access and without any secret value
being exposed

## Linked Issues or Issue Description

No pre-existing public issue. Describing inline per the feature request
template:

**Subsystem affected:** `server/` — REST API & orchestration services

**Problem or motivation:**
Agents that configure env bindings must reference secrets by UUID
(`secretId`). There is no agent-accessible API to resolve a secret name
to its UUID. `GET /companies/:companyId/secrets` requires board access;
the internal `secrets.resolve` handler rejects anything that is not
already a UUID. Agents and their operators are forced to find UUIDs by
inspecting browser network requests, which is friction that should not
exist.

**Proposed solution:**
Add a read-only catalog endpoint — `GET
/companies/:companyId/secrets/catalog` — that agents can call. It
returns only non-sensitive metadata (`id`, `name`, `key`, `status`) for
each active company secret, stripped of values, provider configuration,
and version history. Board callers get the same response. The existing
full-detail list endpoint (`GET /companies/:companyId/secrets`) remains
board-only and is unchanged.

**Alternatives considered:**
- Allow agents to call the existing `/secrets` list — rejected because
it returns full rows including provider metadata; narrowing the response
is safer.
- Add a name-to-UUID lookup by query param — simpler but less useful; a
full catalog means the agent can do the resolution locally without a
second round-trip.

**Roadmap alignment:** Does not duplicate anything in `ROADMAP.md`.

## What Changed

- `server/src/routes/secrets.ts` — new `GET
/companies/:companyId/secrets/catalog` route registered before the
board-only `GET /companies/:companyId/secrets` route. Uses
`assertBoardOrAgent` + `assertCompanyAccess`. Calls `svc.list()` then
projects each row to `{ id, name, key, status }` before responding.
- `server/src/__tests__/secrets-routes.test.ts` — adds `list` to the
shared mock service object (it was missing); adds a `describe` block
with four test cases: board caller receives stripped metadata, agent
caller in the same company receives stripped metadata, unauthenticated
request gets 401, agent from a different company gets 403.

## Verification

**Automated:**
```bash
pnpm --filter @paperclipai/server test --run secrets-routes
```
All four new test cases (board access, agent access, unauthed rejection,
cross-company rejection) should pass.

**Manual:**
1. Start the Paperclip server locally.
2. Create a company and a secret via the UI.
3. Call the endpoint as a board user:
   ```bash
curl http://localhost:3100/api/companies/<companyId>/secrets/catalog \
     -H "Authorization: Bearer <board-session-token>"
   ```
Expect a JSON array with `id`, `name`, `key`, `status` fields — no
`provider`, no `referenceCount`, no version data.
4. Call the same endpoint with an agent API key:
   ```bash
curl http://localhost:3100/api/companies/<companyId>/secrets/catalog \
     -H "Authorization: Bearer <agent-api-key>"
   ```
   Expect the same response.
5. Call with an agent API key scoped to a *different* company — expect
403.

## Risks

Low risk. This is a purely additive, read-only endpoint. No existing
behavior changes. The only new capability is that agents can discover
the UUIDs of secrets in their own company — metadata they already need
to do their job. Secret values are never returned. Authorization reuses
the existing `assertBoardOrAgent` and `assertCompanyAccess` guards
already used throughout the codebase.

## Model Used

Claude Sonnet 4.6 (`claude-sonnet-4-6`) — Anthropic, extended context,
tool use enabled. The entire change (route, tests, PR description) was
produced by the model operating as a Paperclip CEO agent assigned to the
task.

## Checklist

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

---------

Co-authored-by: Austin Pilz <austinpilz@users.noreply.github.com>
Co-authored-by: root <root@paperclip.pilz.dev>
Co-authored-by: Internet Historian <agent@paperclip.internal>
Co-authored-by: Andrew Aymeloglu <aaymeloglu@gmail.com>
2026-08-13 17:43:43 -05:00
scotttong eabecc6f77
feat(annotations): include issue document annotations in agent review context (#11332)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Reviewers annotate plans and issue documents with inline comments,
and assigned agents act on that feedback
> - The server already builds a bounded review context from open plan
annotations and includes it in agent wake payloads
> - Non-plan issue documents did not get the same treatment: their open
annotation threads never reached the agent, and the properties pane did
not surface their annotations
> - This pull request extends the review-context path and the
properties-pane UI to issue documents, at parity with plans
> - The benefit is that agent feedback on any issue document reaches the
assigned agent, not only feedback on the plan

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The review-context pipeline that delivers inline annotation feedback to
assigned agents, and the properties pane that surfaces those annotations
to reviewers.

**Subsystem affected**

The server review-context path
(`server/src/services/plan-review-context.ts`, wake payload assembly in
`server/src/services/heartbeat.ts`, `server/src/routes/issues.ts`),
shared wake-payload types (`packages/shared`, `packages/adapter-utils`),
and the issue properties pane (`ui/src/components/issue-properties/`).

**Current behavior**

A reviewer can annotate any issue document, not only the plan. The agent
wake payload includes open annotation threads for the plan document
only. Feedback left on other issue documents is invisible to the
assigned agent. In the properties pane, the Artifacts tab also gives no
way to see or open a document's annotations.

**Proposed behavior**

Add `buildDocumentReviewContext` beside the existing plan builder. It
collects open annotation threads for all non-plan issue documents,
applies the same thread, comment, and character budgets across
documents, and reports truncation. Include the result as a new
`documentReviewContext` field in agent wake payloads and in the issue
wake-context route. Keep the plan context on its legacy builder and
field so plan-only wakes stay byte-for-byte compatible. Render the new
context in the adapter wake-payload text, and surface annotation counts
and the annotation panel for documents in the properties pane's Plans
and Artifacts tabs.

**Reason and benefit**

The floating annotation popover and persistent highlight UI landed
earlier; this change completes the loop so agent feedback on any issue
document reaches the assigned agent, not only feedback on the plan.

**Breaking changes**

None. The wake payload gains a new optional `documentReviewContext`
field; the existing plan context field and its legacy builder are
unchanged, so plan-only wakes stay byte-for-byte compatible.

## What Changed

- Add `buildDocumentReviewContext` in
`server/src/services/plan-review-context.ts`: bounded review context
(shared thread/comment/character budgets, per-document legacy limits)
over all non-plan issue documents
- Include `documentReviewContext` in agent wake payloads
(`server/src/services/heartbeat.ts`) and in the issue wake-context
response (`server/src/routes/issues.ts`)
- Add shared `DocumentReviewContext` / `DocumentReviewContextDocument`
types in `packages/shared`
- Normalize and render the new context in adapter wake-payload text
(`packages/adapter-utils/src/server-utils.ts`), with tests
- Show a `DocumentAnnotationsCountChip` and the annotation panel for
documents in the properties pane Plans and Artifacts tabs, with tests
- Extend server document-annotations service tests to cover the new
context builder

## Verification

- Run `npx vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/document-annotations-service.test.ts` from the repo
root — 104 tests pass
- Run `TZ=UTC npx vitest run
ui/src/components/issue-properties/IssuePropertiesDocumentAnnotations.test.tsx
ui/src/components/IssueProperties.test.tsx
ui/src/components/IssueDocumentAnnotations.test.tsx
ui/src/components/DocumentAnnotationPopover.test.tsx` from the repo root
— 75 tests pass (one pre-existing monitor-row case asserts UTC
timestamps, so use `TZ=UTC` locally; CI runs in UTC)
- `pnpm run typecheck` in `server/` passes
- Manual: annotate a non-plan issue document, then wake the assigned
agent with a comment — the wake payload lists the open document
annotation threads; the Artifacts tab shows the annotation count chip
and opens the panel

## Risks

- The wake payload gains a new optional `documentReviewContext` field;
consumers that ignore unknown fields are unaffected, and the plan
context field is unchanged
- The context is new input to agent wakes; shared budgets (same limits
as the plan context) bound token cost across all documents
- Low UI risk: the properties-pane changes reuse the existing annotation
components

> 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

- Claude (Anthropic), model ID `claude-fable-5` (Claude Fable 5), with
extended thinking and agentic tool use (Claude Code 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-13 14:23:09 -07:00
Devin Foley 9b1fd42ac1
test(grok-local): isolate billing env in usage cost test (#11285)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Local adapters report run output, token use, and cost data.
> - The Grok local adapter now reports real token use and cost data.
> - Its new billing test must prove the no-key path and the API-key
path.
> - The no-key assertion used the caller environment without isolation.
> - This made the test fail when `XAI_API_KEY` was already set.
> - This pull request isolates that environment state in the test.
> - The benefit is stable coverage for the cost gate from #10433.

## Linked Issues or Issue Description

Refs #10433

**What happened?**

The Grok local usage and cost test asserted subscription billing while
it still used the ambient process environment. If `XAI_API_KEY` was set
before the test ran, the adapter selected API billing instead. The
subscription assertion could then fail on a developer machine or a CI
runner with provider credentials.

**Expected behavior**

The test should prove the subscription path with no `XAI_API_KEY`. It
should also prove the API billing path with a test key.

**Steps to reproduce**

1. Start from `master` after #10433.
2. Set `XAI_API_KEY` in the shell environment.
3. Run `vitest` for
`packages/adapters/grok-local/src/server/execute.test.ts`.
4. Observe that the subscription half can take the API billing branch
without test isolation.

**Paperclip version or commit**

`master` after #10433.

**Deployment mode**

Built from source.

## What Changed

- Isolated `XAI_API_KEY` with save, delete, set, and restore logic
around both billing assertions.
- Gave the subscription and API billing checks separate run ids and temp
roots.

## Verification

- `XAI_API_KEY=ambient-test-key corepack pnpm exec vitest run
packages/adapters/grok-local/src/server/execute.test.ts`
- `corepack pnpm --filter @paperclipai/adapter-grok-local typecheck`

## Risks

Low risk. This changes test setup only. It does not change Grok local
adapter runtime 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 GPT-5 Codex local coding agent. The agent used shell tools,
GitHub CLI, and local test execution. The context window size was not
exposed in this run.

## Checklist

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

Co-authored-by: Claude <noreply@paperclip.ing>
2026-08-13 13:58:05 -07:00