2026-03-23 12:35:08 -05:00
import { execFileSync } from "node:child_process" ;
import { promises as fs } from "node:fs" ;
import os from "node:os" ;
import path from "node:path" ;
2026-03-19 07:24:04 -05:00
import { Readable } from "node:stream" ;
2026-03-14 09:46:16 -05:00
import { beforeEach , describe , expect , it , vi } from "vitest" ;
2026-03-19 07:28:26 -05:00
import type { CompanyPortabilityFileEntry } from "@paperclipai/shared" ;
2026-03-14 09:46:16 -05:00
const companySvc = {
getById : vi.fn ( ) ,
create : vi.fn ( ) ,
update : vi.fn ( ) ,
} ;
const agentSvc = {
list : vi.fn ( ) ,
create : vi.fn ( ) ,
update : vi.fn ( ) ,
} ;
const accessSvc = {
ensureMembership : vi.fn ( ) ,
2026-03-18 21:54:10 -05:00
listActiveUserMemberships : vi.fn ( ) ,
copyActiveUserMemberships : vi.fn ( ) ,
2026-03-20 07:01:42 -05:00
setPrincipalPermission : vi.fn ( ) ,
2026-03-14 09:46:16 -05:00
} ;
const projectSvc = {
list : vi.fn ( ) ,
create : vi.fn ( ) ,
update : vi.fn ( ) ,
2026-03-23 11:14:01 -05:00
createWorkspace : vi.fn ( ) ,
listWorkspaces : vi.fn ( ) ,
2026-03-14 09:46:16 -05:00
} ;
const issueSvc = {
list : vi.fn ( ) ,
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
listComments : vi.fn ( ) ,
2026-03-14 09:46:16 -05:00
getById : vi.fn ( ) ,
getByIdentifier : vi.fn ( ) ,
create : vi.fn ( ) ,
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
addComment : vi.fn ( ) ,
2026-03-14 09:46:16 -05:00
} ;
2026-03-23 11:14:01 -05:00
const routineSvc = {
list : vi.fn ( ) ,
getDetail : vi.fn ( ) ,
create : vi.fn ( ) ,
createTrigger : vi.fn ( ) ,
} ;
2026-03-14 18:59:26 -05:00
const companySkillSvc = {
list : vi.fn ( ) ,
2026-03-16 17:45:28 -05:00
listFull : vi.fn ( ) ,
2026-03-14 18:59:26 -05:00
readFile : vi.fn ( ) ,
importPackageFiles : vi.fn ( ) ,
} ;
2026-03-19 07:24:04 -05:00
const assetSvc = {
getById : vi.fn ( ) ,
create : vi.fn ( ) ,
} ;
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
const secretSvc = {
normalizeAdapterConfigForPersistence : vi.fn ( async ( _companyId : string , config : Record < string , unknown > ) = > config ) ,
resolveAdapterConfigForRuntime : vi.fn ( async ( _companyId : string , config : Record < string , unknown > ) = > ( { config , secretKeys : new Set < string > ( ) } ) ) ,
} ;
2026-03-17 13:42:00 -05:00
const agentInstructionsSvc = {
exportFiles : vi.fn ( ) ,
materializeManagedBundle : vi.fn ( ) ,
} ;
2026-03-14 09:46:16 -05:00
vi . mock ( "../services/companies.js" , ( ) = > ( {
companyService : ( ) = > companySvc ,
} ) ) ;
vi . mock ( "../services/agents.js" , ( ) = > ( {
agentService : ( ) = > agentSvc ,
} ) ) ;
vi . mock ( "../services/access.js" , ( ) = > ( {
accessService : ( ) = > accessSvc ,
} ) ) ;
vi . mock ( "../services/projects.js" , ( ) = > ( {
projectService : ( ) = > projectSvc ,
} ) ) ;
vi . mock ( "../services/issues.js" , ( ) = > ( {
issueService : ( ) = > issueSvc ,
} ) ) ;
2026-03-23 11:14:01 -05:00
vi . mock ( "../services/routines.js" , ( ) = > ( {
routineService : ( ) = > routineSvc ,
} ) ) ;
2026-03-14 18:59:26 -05:00
vi . mock ( "../services/company-skills.js" , ( ) = > ( {
companySkillService : ( ) = > companySkillSvc ,
} ) ) ;
2026-03-19 07:24:04 -05:00
vi . mock ( "../services/assets.js" , ( ) = > ( {
assetService : ( ) = > assetSvc ,
} ) ) ;
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
vi . mock ( "../services/secrets.js" , ( ) = > ( {
secretService : ( ) = > secretSvc ,
} ) ) ;
2026-03-17 13:42:00 -05:00
vi . mock ( "../services/agent-instructions.js" , ( ) = > ( {
agentInstructionsService : ( ) = > agentInstructionsSvc ,
} ) ) ;
2026-03-20 15:04:55 -05:00
vi . mock ( "../routes/org-chart-svg.js" , ( ) = > ( {
renderOrgChartPng : vi.fn ( async ( ) = > Buffer . from ( "png" ) ) ,
} ) ) ;
2026-03-23 06:47:32 -05:00
const { companyPortabilityService , parseGitHubSourceUrl } = await import ( "../services/company-portability.js" ) ;
2026-03-14 09:46:16 -05:00
2026-03-19 07:28:26 -05:00
function asTextFile ( entry : CompanyPortabilityFileEntry | undefined ) {
expect ( typeof entry ) . toBe ( "string" ) ;
return typeof entry === "string" ? entry : "" ;
}
2026-03-14 09:46:16 -05:00
describe ( "company portability" , ( ) = > {
2026-03-16 18:27:20 -05:00
const paperclipKey = "paperclipai/paperclip/paperclip" ;
const companyPlaybookKey = "company/company-1/company-playbook" ;
2026-03-14 09:46:16 -05:00
beforeEach ( ( ) = > {
vi . clearAllMocks ( ) ;
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
secretSvc . normalizeAdapterConfigForPersistence . mockImplementation ( async ( _companyId , config ) = > config ) ;
secretSvc . resolveAdapterConfigForRuntime . mockImplementation ( async ( _companyId , config ) = > ( {
config ,
secretKeys : new Set < string > ( ) ,
} ) ) ;
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
issueSvc . listComments . mockResolvedValue ( [ ] ) ;
issueSvc . addComment . mockResolvedValue ( {
id : "comment-imported" ,
body : "Imported comment" ,
authorType : "system" ,
presentation : null ,
metadata : null ,
} ) ;
2026-03-14 09:46:16 -05:00
companySvc . getById . mockResolvedValue ( {
id : "company-1" ,
name : "Paperclip" ,
description : null ,
2026-03-18 16:54:25 -05:00
issuePrefix : "PAP" ,
2026-03-14 09:46:16 -05:00
brandColor : "#5c5fff" ,
2026-03-19 07:24:04 -05:00
logoAssetId : null ,
logoUrl : null ,
[codex] Harden recovery issue handling (#4600)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane must recover stranded agent work without creating
new operational loops
> - Stranded recovery issues can themselves fail, and exposing raw retry
errors in comments can leak sensitive adapter details
> - New local companies also should not force a hire-approval gate
unless operators enable that policy
> - This pull request hardens recovery issue handling, redacts retry
failure details in issue copy, preserves `maxConcurrentRuns: 1`, and
flips new-hire approval to an opt-in default
> - The benefit is safer automatic recovery and smoother default company
setup without hidden migration conflicts
## What Changed
- Added migration `0071_default_hire_approval_off` and updated company
schema/import/export/docs so hire approvals default off and serialize
only when enabled.
- Added migration `0072_large_sandman` with a partial unique index
preventing duplicate active stranded recovery issues for the same source
issue.
- Blocked failed `stranded_issue_recovery` issues in place instead of
creating nested recovery issues.
- Redacted latest retry failure details from recovery issue comments
while still linking reviewers to run evidence.
- Allowed `maxConcurrentRuns: 1` to be honored by heartbeat concurrency
normalization.
- Added focused regression coverage for recovery recursion, redaction,
migration ordering, and concurrency behavior.
## Verification
- `pnpm --filter @paperclipai/db run check:migrations`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/recovery-classifiers.test.ts`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/company-portability.test.ts --pool=forks
--poolOptions.forks.isolate=true`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/agent-permissions-routes.test.ts --pool=forks
--poolOptions.forks.isolate=true`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-process-recovery.test.ts --pool=forks
--poolOptions.forks.isolate=true` exits 0, but this host skipped the
embedded Postgres tests with the existing init guard.
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-dependency-scheduling.test.ts
--pool=forks --poolOptions.forks.isolate=true` exits 0, but this host
skipped the embedded Postgres tests with the existing init guard.
## Risks
- Migration risk is low but this PR intentionally owns both new
migrations to avoid separate PR migration-journal conflicts.
- Recovery comments now require operators to inspect linked run evidence
for details instead of reading raw errors inline.
- The hire approval default changes behavior for newly created/imported
companies only; existing persisted company settings are not changed
except by the SQL default for future rows.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5 coding agent, tool-enabled terminal/GitHub
workflow, reasoning mode active. Context window not exposed in this
environment.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-27 15:02:47 -05:00
requireBoardApprovalForNewAgents : false ,
2026-03-14 09:46:16 -05:00
} ) ;
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
[codex] Harden recovery issue handling (#4600)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane must recover stranded agent work without creating
new operational loops
> - Stranded recovery issues can themselves fail, and exposing raw retry
errors in comments can leak sensitive adapter details
> - New local companies also should not force a hire-approval gate
unless operators enable that policy
> - This pull request hardens recovery issue handling, redacts retry
failure details in issue copy, preserves `maxConcurrentRuns: 1`, and
flips new-hire approval to an opt-in default
> - The benefit is safer automatic recovery and smoother default company
setup without hidden migration conflicts
## What Changed
- Added migration `0071_default_hire_approval_off` and updated company
schema/import/export/docs so hire approvals default off and serialize
only when enabled.
- Added migration `0072_large_sandman` with a partial unique index
preventing duplicate active stranded recovery issues for the same source
issue.
- Blocked failed `stranded_issue_recovery` issues in place instead of
creating nested recovery issues.
- Redacted latest retry failure details from recovery issue comments
while still linking reviewers to run evidence.
- Allowed `maxConcurrentRuns: 1` to be honored by heartbeat concurrency
normalization.
- Added focused regression coverage for recovery recursion, redaction,
migration ordering, and concurrency behavior.
## Verification
- `pnpm --filter @paperclipai/db run check:migrations`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/recovery-classifiers.test.ts`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/company-portability.test.ts --pool=forks
--poolOptions.forks.isolate=true`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/agent-permissions-routes.test.ts --pool=forks
--poolOptions.forks.isolate=true`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-process-recovery.test.ts --pool=forks
--poolOptions.forks.isolate=true` exits 0, but this host skipped the
embedded Postgres tests with the existing init guard.
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-dependency-scheduling.test.ts
--pool=forks --poolOptions.forks.isolate=true` exits 0, but this host
skipped the embedded Postgres tests with the existing init guard.
## Risks
- Migration risk is low but this PR intentionally owns both new
migrations to avoid separate PR migration-journal conflicts.
- Recovery comments now require operators to inspect linked run evidence
for details instead of reading raw errors inline.
- The hire approval default changes behavior for newly created/imported
companies only; existing persisted company settings are not changed
except by the SQL default for future rows.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5 coding agent, tool-enabled terminal/GitHub
workflow, reasoning mode active. Context window not exposed in this
environment.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-27 15:02:47 -05:00
requireBoardApprovalForNewAgents : false ,
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
} ) ;
2026-03-14 09:46:16 -05:00
agentSvc . list . mockResolvedValue ( [
{
id : "agent-1" ,
name : "ClaudeCoder" ,
status : "idle" ,
role : "engineer" ,
title : "Software Engineer" ,
icon : "code" ,
reportsTo : null ,
capabilities : "Writes code" ,
adapterType : "claude_local" ,
adapterConfig : {
promptTemplate : "You are ClaudeCoder." ,
2026-03-14 18:59:26 -05:00
paperclipSkillSync : {
2026-03-16 18:27:20 -05:00
desiredSkills : [ paperclipKey ] ,
2026-03-14 18:59:26 -05:00
} ,
2026-03-14 09:46:16 -05:00
instructionsFilePath : "/tmp/ignored.md" ,
cwd : "/tmp/ignored" ,
command : "/Users/dotta/.local/bin/claude" ,
model : "claude-opus-4-6" ,
env : {
ANTHROPIC_API_KEY : {
type : "secret_ref" ,
secretId : "secret-1" ,
version : "latest" ,
} ,
GH_TOKEN : {
type : "secret_ref" ,
secretId : "secret-2" ,
version : "latest" ,
} ,
PATH : {
type : "plain" ,
value : "/usr/bin:/bin" ,
} ,
} ,
} ,
runtimeConfig : {
heartbeat : {
intervalSec : 3600 ,
} ,
} ,
budgetMonthlyCents : 0 ,
permissions : {
canCreateAgents : false ,
} ,
metadata : null ,
} ,
2026-03-14 18:59:26 -05:00
{
id : "agent-2" ,
name : "CMO" ,
status : "idle" ,
role : "cmo" ,
title : "Chief Marketing Officer" ,
icon : "globe" ,
reportsTo : null ,
capabilities : "Owns marketing" ,
adapterType : "claude_local" ,
adapterConfig : {
promptTemplate : "You are CMO." ,
} ,
runtimeConfig : {
heartbeat : {
intervalSec : 3600 ,
} ,
} ,
budgetMonthlyCents : 0 ,
permissions : {
canCreateAgents : false ,
} ,
metadata : null ,
} ,
2026-03-14 09:46:16 -05:00
] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
2026-03-23 11:14:01 -05:00
projectSvc . createWorkspace . mockResolvedValue ( null ) ;
projectSvc . listWorkspaces . mockResolvedValue ( [ ] ) ;
2026-03-14 09:46:16 -05:00
issueSvc . list . mockResolvedValue ( [ ] ) ;
issueSvc . getById . mockResolvedValue ( null ) ;
issueSvc . getByIdentifier . mockResolvedValue ( null ) ;
2026-03-23 11:14:01 -05:00
routineSvc . list . mockResolvedValue ( [ ] ) ;
routineSvc . getDetail . mockImplementation ( async ( id : string ) = > {
const rows = await routineSvc . list ( ) ;
return rows . find ( ( row : { id : string } ) = > row . id === id ) ? ? null ;
} ) ;
routineSvc . create . mockImplementation ( async ( _companyId : string , input : Record < string , unknown > ) = > ( {
id : "routine-created" ,
companyId : "company-1" ,
projectId : input.projectId ,
goalId : null ,
parentIssueId : null ,
title : input.title ,
description : input.description ? ? null ,
assigneeAgentId : input.assigneeAgentId ,
priority : input.priority ? ? "medium" ,
status : input.status ? ? "active" ,
concurrencyPolicy : input.concurrencyPolicy ? ? "coalesce_if_active" ,
catchUpPolicy : input.catchUpPolicy ? ? "skip_missed" ,
createdByAgentId : null ,
createdByUserId : null ,
updatedByAgentId : null ,
updatedByUserId : null ,
lastTriggeredAt : null ,
lastEnqueuedAt : null ,
createdAt : new Date ( ) ,
updatedAt : new Date ( ) ,
} ) ) ;
routineSvc . createTrigger . mockImplementation ( async ( _routineId : string , input : Record < string , unknown > ) = > ( {
id : "trigger-created" ,
companyId : "company-1" ,
routineId : "routine-created" ,
kind : input.kind ,
label : input.label ? ? null ,
enabled : input.enabled ? ? true ,
cronExpression : input.kind === "schedule" ? input . cronExpression ? ? null : null ,
timezone : input.kind === "schedule" ? input . timezone ? ? null : null ,
nextRunAt : null ,
lastFiredAt : null ,
publicId : null ,
secretId : null ,
signingMode : input.kind === "webhook" ? input . signingMode ? ? "bearer" : null ,
replayWindowSec : input.kind === "webhook" ? input . replayWindowSec ? ? 300 : null ,
lastRotatedAt : null ,
lastResult : null ,
createdByAgentId : null ,
createdByUserId : null ,
updatedByAgentId : null ,
updatedByUserId : null ,
createdAt : new Date ( ) ,
updatedAt : new Date ( ) ,
} ) ) ;
2026-03-16 17:45:28 -05:00
const companySkills = [
2026-03-14 18:59:26 -05:00
{
id : "skill-1" ,
companyId : "company-1" ,
2026-03-16 18:27:20 -05:00
key : paperclipKey ,
2026-03-14 18:59:26 -05:00
slug : "paperclip" ,
name : "paperclip" ,
description : "Paperclip coordination skill" ,
markdown : "---\nname: paperclip\ndescription: Paperclip coordination skill\n---\n\n# Paperclip\n" ,
sourceType : "github" ,
sourceLocator : "https://github.com/paperclipai/paperclip/tree/master/skills/paperclip" ,
sourceRef : "0123456789abcdef0123456789abcdef01234567" ,
trustLevel : "markdown_only" ,
compatibility : "compatible" ,
fileInventory : [
{ path : "SKILL.md" , kind : "skill" } ,
{ path : "references/api.md" , kind : "reference" } ,
] ,
metadata : {
sourceKind : "github" ,
owner : "paperclipai" ,
repo : "paperclip" ,
ref : "0123456789abcdef0123456789abcdef01234567" ,
trackingRef : "master" ,
repoSkillDir : "skills/paperclip" ,
} ,
} ,
{
id : "skill-2" ,
companyId : "company-1" ,
2026-03-16 18:27:20 -05:00
key : companyPlaybookKey ,
2026-03-14 18:59:26 -05:00
slug : "company-playbook" ,
name : "company-playbook" ,
description : "Internal company skill" ,
markdown : "---\nname: company-playbook\ndescription: Internal company skill\n---\n\n# Company Playbook\n" ,
sourceType : "local_path" ,
sourceLocator : "/tmp/company-playbook" ,
sourceRef : null ,
trustLevel : "markdown_only" ,
compatibility : "compatible" ,
fileInventory : [
{ path : "SKILL.md" , kind : "skill" } ,
{ path : "references/checklist.md" , kind : "reference" } ,
] ,
metadata : {
sourceKind : "local_path" ,
} ,
} ,
2026-03-16 17:45:28 -05:00
] ;
companySkillSvc . list . mockResolvedValue ( companySkills ) ;
companySkillSvc . listFull . mockResolvedValue ( companySkills ) ;
2026-03-14 18:59:26 -05:00
companySkillSvc . readFile . mockImplementation ( async ( _companyId : string , skillId : string , relativePath : string ) = > {
if ( skillId === "skill-2" ) {
return {
skillId ,
path : relativePath ,
kind : relativePath === "SKILL.md" ? "skill" : "reference" ,
content : relativePath === "SKILL.md"
? "---\nname: company-playbook\ndescription: Internal company skill\n---\n\n# Company Playbook\n"
: "# Checklist\n" ,
language : "markdown" ,
markdown : true ,
editable : true ,
} ;
}
return {
skillId ,
path : relativePath ,
kind : relativePath === "SKILL.md" ? "skill" : "reference" ,
content : relativePath === "SKILL.md"
? "---\nname: paperclip\ndescription: Paperclip coordination skill\n---\n\n# Paperclip\n"
: "# API\n" ,
language : "markdown" ,
markdown : true ,
editable : false ,
} ;
} ) ;
companySkillSvc . importPackageFiles . mockResolvedValue ( [ ] ) ;
2026-03-19 07:24:04 -05:00
assetSvc . getById . mockReset ( ) ;
assetSvc . getById . mockResolvedValue ( null ) ;
assetSvc . create . mockReset ( ) ;
2026-03-20 07:01:42 -05:00
accessSvc . setPrincipalPermission . mockResolvedValue ( undefined ) ;
2026-03-19 07:24:04 -05:00
assetSvc . create . mockResolvedValue ( {
id : "asset-created" ,
} ) ;
2026-03-18 21:54:10 -05:00
accessSvc . listActiveUserMemberships . mockResolvedValue ( [
{
id : "membership-1" ,
companyId : "company-1" ,
principalType : "user" ,
principalId : "user-1" ,
membershipRole : "owner" ,
status : "active" ,
} ,
] ) ;
accessSvc . copyActiveUserMemberships . mockResolvedValue ( [ ] ) ;
2026-03-17 13:42:00 -05:00
agentInstructionsSvc . exportFiles . mockImplementation ( async ( agent : { name : string } ) = > ( {
files : { "AGENTS.md" : agent . name === "CMO" ? "You are CMO." : "You are ClaudeCoder." } ,
entryFile : "AGENTS.md" ,
warnings : [ ] ,
} ) ) ;
agentInstructionsSvc . materializeManagedBundle . mockImplementation ( async ( agent : { adapterConfig : Record < string , unknown > } ) = > ( {
bundle : null ,
adapterConfig : {
. . . agent . adapterConfig ,
instructionsBundleMode : "managed" ,
instructionsRootPath : ` /tmp/ ${ agent . id } ` ,
instructionsEntryFile : "AGENTS.md" ,
instructionsFilePath : ` /tmp/ ${ agent . id } /AGENTS.md ` ,
} ,
} ) ) ;
2026-03-14 09:46:16 -05:00
} ) ;
2026-03-23 06:47:32 -05:00
it ( "parses canonical GitHub import URLs with explicit ref and package path" , ( ) = > {
expect (
parseGitHubSourceUrl ( "https://github.com/paperclipai/companies?ref=feature%2Fdemo&path=gstack" ) ,
) . toEqual ( {
2026-04-01 20:42:48 +00:00
hostname : "github.com" ,
2026-03-23 06:47:32 -05:00
owner : "paperclipai" ,
repo : "companies" ,
ref : "feature/demo" ,
basePath : "gstack" ,
companyPath : "gstack/COMPANY.md" ,
} ) ;
} ) ;
it ( "parses canonical GitHub import URLs with explicit companyPath" , ( ) = > {
expect (
parseGitHubSourceUrl (
"https://github.com/paperclipai/companies?ref=abc123&companyPath=gstack%2FCOMPANY.md" ,
) ,
) . toEqual ( {
2026-04-01 20:42:48 +00:00
hostname : "github.com" ,
2026-03-23 06:47:32 -05:00
owner : "paperclipai" ,
repo : "companies" ,
ref : "abc123" ,
basePath : "gstack" ,
companyPath : "gstack/COMPANY.md" ,
} ) ;
} ) ;
2026-03-14 18:59:26 -05:00
it ( "exports referenced skills as stubs by default with sanitized Paperclip extension data" , async ( ) = > {
2026-03-14 09:46:16 -05:00
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
2026-03-19 07:28:26 -05:00
expect ( asTextFile ( exported . files [ "COMPANY.md" ] ) ) . toContain ( 'name: "Paperclip"' ) ;
expect ( asTextFile ( exported . files [ "COMPANY.md" ] ) ) . toContain ( 'schema: "agentcompanies/v1"' ) ;
expect ( asTextFile ( exported . files [ "agents/claudecoder/AGENTS.md" ] ) ) . toContain ( "You are ClaudeCoder." ) ;
expect ( asTextFile ( exported . files [ "agents/claudecoder/AGENTS.md" ] ) ) . toContain ( "skills:" ) ;
expect ( asTextFile ( exported . files [ "agents/claudecoder/AGENTS.md" ] ) ) . toContain ( ` - " ${ paperclipKey } " ` ) ;
expect ( asTextFile ( exported . files [ "agents/cmo/AGENTS.md" ] ) ) . not . toContain ( "skills:" ) ;
expect ( asTextFile ( exported . files [ "skills/paperclipai/paperclip/paperclip/SKILL.md" ] ) ) . toContain ( "metadata:" ) ;
expect ( asTextFile ( exported . files [ "skills/paperclipai/paperclip/paperclip/SKILL.md" ] ) ) . toContain ( 'kind: "github-dir"' ) ;
2026-03-18 16:54:25 -05:00
expect ( exported . files [ "skills/paperclipai/paperclip/paperclip/references/api.md" ] ) . toBeUndefined ( ) ;
2026-03-19 07:28:26 -05:00
expect ( asTextFile ( exported . files [ "skills/company/PAP/company-playbook/SKILL.md" ] ) ) . toContain ( "# Company Playbook" ) ;
expect ( asTextFile ( exported . files [ "skills/company/PAP/company-playbook/references/checklist.md" ] ) ) . toContain ( "# Checklist" ) ;
2026-03-14 09:46:16 -05:00
2026-03-19 07:28:26 -05:00
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
2026-03-14 09:46:16 -05:00
expect ( extension ) . toContain ( 'schema: "paperclip/v1"' ) ;
expect ( extension ) . not . toContain ( "promptTemplate" ) ;
expect ( extension ) . not . toContain ( "instructionsFilePath" ) ;
expect ( extension ) . not . toContain ( "command:" ) ;
expect ( extension ) . not . toContain ( "secretId" ) ;
expect ( extension ) . not . toContain ( 'type: "secret_ref"' ) ;
expect ( extension ) . toContain ( "inputs:" ) ;
expect ( extension ) . toContain ( "ANTHROPIC_API_KEY:" ) ;
expect ( extension ) . toContain ( 'requirement: "optional"' ) ;
expect ( extension ) . toContain ( 'default: ""' ) ;
2026-03-16 08:55:37 -05:00
expect ( extension ) . not . toContain ( "paperclipSkillSync" ) ;
2026-03-14 09:46:16 -05:00
expect ( extension ) . not . toContain ( "PATH:" ) ;
expect ( extension ) . not . toContain ( "requireBoardApprovalForNewAgents: true" ) ;
expect ( extension ) . not . toContain ( "budgetMonthlyCents: 0" ) ;
expect ( exported . warnings ) . toContain ( "Agent claudecoder command /Users/dotta/.local/bin/claude was omitted from export because it is system-dependent." ) ;
expect ( exported . warnings ) . toContain ( "Agent claudecoder PATH override was omitted from export because it is system-dependent." ) ;
} ) ;
[codex] Harden recovery issue handling (#4600)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane must recover stranded agent work without creating
new operational loops
> - Stranded recovery issues can themselves fail, and exposing raw retry
errors in comments can leak sensitive adapter details
> - New local companies also should not force a hire-approval gate
unless operators enable that policy
> - This pull request hardens recovery issue handling, redacts retry
failure details in issue copy, preserves `maxConcurrentRuns: 1`, and
flips new-hire approval to an opt-in default
> - The benefit is safer automatic recovery and smoother default company
setup without hidden migration conflicts
## What Changed
- Added migration `0071_default_hire_approval_off` and updated company
schema/import/export/docs so hire approvals default off and serialize
only when enabled.
- Added migration `0072_large_sandman` with a partial unique index
preventing duplicate active stranded recovery issues for the same source
issue.
- Blocked failed `stranded_issue_recovery` issues in place instead of
creating nested recovery issues.
- Redacted latest retry failure details from recovery issue comments
while still linking reviewers to run evidence.
- Allowed `maxConcurrentRuns: 1` to be honored by heartbeat concurrency
normalization.
- Added focused regression coverage for recovery recursion, redaction,
migration ordering, and concurrency behavior.
## Verification
- `pnpm --filter @paperclipai/db run check:migrations`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/recovery-classifiers.test.ts`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/company-portability.test.ts --pool=forks
--poolOptions.forks.isolate=true`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/agent-permissions-routes.test.ts --pool=forks
--poolOptions.forks.isolate=true`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-process-recovery.test.ts --pool=forks
--poolOptions.forks.isolate=true` exits 0, but this host skipped the
embedded Postgres tests with the existing init guard.
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-dependency-scheduling.test.ts
--pool=forks --poolOptions.forks.isolate=true` exits 0, but this host
skipped the embedded Postgres tests with the existing init guard.
## Risks
- Migration risk is low but this PR intentionally owns both new
migrations to avoid separate PR migration-journal conflicts.
- Recovery comments now require operators to inspect linked run evidence
for details instead of reading raw errors inline.
- The hire approval default changes behavior for newly created/imported
companies only; existing persisted company settings are not changed
except by the SQL default for future rows.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5 coding agent, tool-enabled terminal/GitHub
workflow, reasoning mode active. Context window not exposed in this
environment.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-27 15:02:47 -05:00
it ( "exports hire approval policy only when approval is required" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . getById . mockResolvedValueOnce ( {
id : "company-1" ,
name : "Paperclip" ,
description : null ,
issuePrefix : "PAP" ,
brandColor : "#5c5fff" ,
logoAssetId : null ,
logoUrl : null ,
requireBoardApprovalForNewAgents : true ,
} ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : false ,
projects : false ,
issues : false ,
} ,
} ) ;
expect ( asTextFile ( exported . files [ ".paperclip.yaml" ] ) ) . toContain ( "requireBoardApprovalForNewAgents: true" ) ;
} ) ;
2026-03-23 16:49:46 -05:00
it ( "exports default sidebar order into the Paperclip extension and manifest" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-2" ,
companyId : "company-1" ,
name : "Zulu" ,
urlKey : "zulu" ,
description : null ,
leadAgentId : null ,
targetDate : null ,
color : null ,
status : "planned" ,
executionWorkspacePolicy : null ,
archivedAt : null ,
workspaces : [ ] ,
} ,
{
id : "project-1" ,
companyId : "company-1" ,
name : "Alpha" ,
urlKey : "alpha" ,
description : null ,
leadAgentId : null ,
targetDate : null ,
color : null ,
status : "planned" ,
executionWorkspacePolicy : null ,
archivedAt : null ,
workspaces : [ ] ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : true ,
issues : false ,
} ,
} ) ;
expect ( asTextFile ( exported . files [ ".paperclip.yaml" ] ) ) . toContain ( [
"sidebar:" ,
" agents:" ,
' - "claudecoder"' ,
' - "cmo"' ,
" projects:" ,
' - "alpha"' ,
' - "zulu"' ,
] . join ( "\n" ) ) ;
expect ( exported . manifest . sidebar ) . toEqual ( {
agents : [ "claudecoder" , "cmo" ] ,
projects : [ "alpha" , "zulu" ] ,
} ) ;
} ) ;
2026-03-14 18:59:26 -05:00
it ( "expands referenced skills when requested" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
expandReferencedSkills : true ,
} ) ;
2026-03-19 07:28:26 -05:00
expect ( asTextFile ( exported . files [ "skills/paperclipai/paperclip/paperclip/SKILL.md" ] ) ) . toContain ( "# Paperclip" ) ;
expect ( asTextFile ( exported . files [ "skills/paperclipai/paperclip/paperclip/SKILL.md" ] ) ) . toContain ( "metadata:" ) ;
expect ( asTextFile ( exported . files [ "skills/paperclipai/paperclip/paperclip/references/api.md" ] ) ) . toContain ( "# API" ) ;
2026-03-18 16:23:19 -05:00
} ) ;
2026-03-19 16:29:11 -05:00
it ( "exports only selected skills when skills filter is provided" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
skills : [ "company-playbook" ] ,
} ) ;
expect ( exported . files [ "skills/company/PAP/company-playbook/SKILL.md" ] ) . toBeDefined ( ) ;
expect ( asTextFile ( exported . files [ "skills/company/PAP/company-playbook/SKILL.md" ] ) ) . toContain ( "# Company Playbook" ) ;
expect ( exported . files [ "skills/paperclipai/paperclip/paperclip/SKILL.md" ] ) . toBeUndefined ( ) ;
} ) ;
it ( "warns and exports all skills when skills filter matches nothing" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
skills : [ "nonexistent-skill" ] ,
} ) ;
expect ( exported . warnings ) . toContainEqual ( expect . stringContaining ( "nonexistent-skill" ) ) ;
expect ( exported . files [ "skills/company/PAP/company-playbook/SKILL.md" ] ) . toBeDefined ( ) ;
expect ( exported . files [ "skills/paperclipai/paperclip/paperclip/SKILL.md" ] ) . toBeDefined ( ) ;
2026-03-18 16:23:19 -05:00
} ) ;
2026-03-19 07:24:04 -05:00
it ( "exports the company logo into images/ and references it from .paperclip.yaml" , async ( ) = > {
const storage = {
getObject : vi.fn ( ) . mockResolvedValue ( {
stream : Readable.from ( [ Buffer . from ( "png-bytes" ) ] ) ,
} ) ,
} ;
companySvc . getById . mockResolvedValue ( {
id : "company-1" ,
name : "Paperclip" ,
description : null ,
issuePrefix : "PAP" ,
brandColor : "#5c5fff" ,
logoAssetId : "logo-1" ,
logoUrl : "/api/assets/logo-1/content" ,
requireBoardApprovalForNewAgents : true ,
} ) ;
assetSvc . getById . mockResolvedValue ( {
id : "logo-1" ,
companyId : "company-1" ,
objectKey : "assets/companies/logo-1" ,
contentType : "image/png" ,
originalFilename : "logo.png" ,
} ) ;
const portability = companyPortabilityService ( { } as any , storage as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : false ,
projects : false ,
issues : false ,
} ,
} ) ;
expect ( storage . getObject ) . toHaveBeenCalledWith ( "company-1" , "assets/companies/logo-1" ) ;
expect ( exported . files [ "images/company-logo.png" ] ) . toEqual ( {
encoding : "base64" ,
data : Buffer.from ( "png-bytes" ) . toString ( "base64" ) ,
contentType : "image/png" ,
} ) ;
expect ( exported . files [ ".paperclip.yaml" ] ) . toContain ( 'logoPath: "images/company-logo.png"' ) ;
} ) ;
2026-03-18 16:54:25 -05:00
it ( "exports duplicate skill slugs into readable namespaced paths" , async ( ) = > {
2026-03-18 16:23:19 -05:00
const portability = companyPortabilityService ( { } as any ) ;
companySkillSvc . readFile . mockImplementation ( async ( _companyId : string , skillId : string , relativePath : string ) = > {
if ( skillId === "skill-local" ) {
return {
skillId ,
path : relativePath ,
kind : "skill" ,
content : "---\nname: release-changelog\n---\n\n# Local Release Changelog\n" ,
language : "markdown" ,
markdown : true ,
editable : true ,
} ;
}
return {
skillId ,
path : relativePath ,
kind : "skill" ,
content : "---\nname: release-changelog\n---\n\n# Bundled Release Changelog\n" ,
language : "markdown" ,
markdown : true ,
editable : false ,
} ;
} ) ;
companySkillSvc . listFull . mockResolvedValue ( [
{
id : "skill-local" ,
companyId : "company-1" ,
key : "local/36dfd631da/release-changelog" ,
slug : "release-changelog" ,
name : "release-changelog" ,
description : "Local release changelog skill" ,
markdown : "---\nname: release-changelog\n---\n\n# Local Release Changelog\n" ,
sourceType : "local_path" ,
sourceLocator : "/tmp/release-changelog" ,
sourceRef : null ,
trustLevel : "markdown_only" ,
compatibility : "compatible" ,
fileInventory : [ { path : "SKILL.md" , kind : "skill" } ] ,
metadata : {
sourceKind : "local_path" ,
} ,
} ,
{
id : "skill-paperclip" ,
companyId : "company-1" ,
key : "paperclipai/paperclip/release-changelog" ,
slug : "release-changelog" ,
name : "release-changelog" ,
description : "Bundled release changelog skill" ,
markdown : "---\nname: release-changelog\n---\n\n# Bundled Release Changelog\n" ,
sourceType : "github" ,
sourceLocator : "https://github.com/paperclipai/paperclip/tree/master/skills/release-changelog" ,
sourceRef : "0123456789abcdef0123456789abcdef01234567" ,
trustLevel : "markdown_only" ,
compatibility : "compatible" ,
fileInventory : [ { path : "SKILL.md" , kind : "skill" } ] ,
metadata : {
sourceKind : "paperclip_bundled" ,
owner : "paperclipai" ,
repo : "paperclip" ,
ref : "0123456789abcdef0123456789abcdef01234567" ,
trackingRef : "master" ,
repoSkillDir : "skills/release-changelog" ,
} ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
2026-03-19 07:28:26 -05:00
expect ( asTextFile ( exported . files [ "skills/local/release-changelog/SKILL.md" ] ) ) . toContain ( "# Local Release Changelog" ) ;
expect ( asTextFile ( exported . files [ "skills/paperclipai/paperclip/release-changelog/SKILL.md" ] ) ) . toContain ( "metadata:" ) ;
expect ( asTextFile ( exported . files [ "skills/paperclipai/paperclip/release-changelog/SKILL.md" ] ) ) . toContain ( "paperclipai/paperclip/release-changelog" ) ;
2026-03-14 18:59:26 -05:00
} ) ;
2026-03-18 21:54:10 -05:00
it ( "builds export previews without tasks by default" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-1" ,
name : "Launch" ,
urlKey : "launch" ,
description : "Ship it" ,
leadAgentId : "agent-1" ,
targetDate : null ,
color : null ,
status : "planned" ,
executionWorkspacePolicy : null ,
archivedAt : null ,
} ,
] ) ;
issueSvc . list . mockResolvedValue ( [
{
id : "issue-1" ,
identifier : "PAP-1" ,
title : "Write launch task" ,
description : "Task body" ,
projectId : "project-1" ,
assigneeAgentId : "agent-1" ,
status : "todo" ,
priority : "medium" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
] ) ;
const preview = await portability . previewExport ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : true ,
} ,
} ) ;
expect ( preview . counts . issues ) . toBe ( 0 ) ;
expect ( preview . fileInventory . some ( ( entry ) = > entry . path . startsWith ( "tasks/" ) ) ) . toBe ( false ) ;
} ) ;
2026-03-23 11:14:01 -05:00
it ( "exports portable project workspace metadata and remaps it on import" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-1" ,
name : "Launch" ,
urlKey : "launch" ,
description : "Ship it" ,
leadAgentId : "agent-1" ,
targetDate : "2026-03-31" ,
color : "#123456" ,
status : "planned" ,
executionWorkspacePolicy : {
enabled : true ,
defaultMode : "shared_workspace" ,
defaultProjectWorkspaceId : "workspace-1" ,
workspaceStrategy : {
type : "project_primary" ,
} ,
} ,
workspaces : [
{
id : "workspace-1" ,
companyId : "company-1" ,
projectId : "project-1" ,
name : "Main Repo" ,
sourceType : "git_repo" ,
cwd : "/Users/dotta/paperclip" ,
repoUrl : "https://github.com/paperclipai/paperclip.git" ,
repoRef : "main" ,
defaultRef : "main" ,
visibility : "default" ,
setupCommand : "pnpm install" ,
cleanupCommand : "rm -rf .paperclip-tmp" ,
remoteProvider : null ,
remoteWorkspaceRef : null ,
sharedWorkspaceKey : null ,
metadata : {
language : "typescript" ,
} ,
isPrimary : true ,
createdAt : new Date ( "2026-03-01T00:00:00Z" ) ,
updatedAt : new Date ( "2026-03-01T00:00:00Z" ) ,
} ,
{
id : "workspace-2" ,
companyId : "company-1" ,
projectId : "project-1" ,
name : "Local Scratch" ,
sourceType : "local_path" ,
cwd : "/tmp/paperclip-local" ,
repoUrl : null ,
repoRef : null ,
defaultRef : null ,
visibility : "advanced" ,
setupCommand : null ,
cleanupCommand : null ,
remoteProvider : null ,
remoteWorkspaceRef : null ,
sharedWorkspaceKey : null ,
metadata : null ,
isPrimary : false ,
createdAt : new Date ( "2026-03-01T00:00:00Z" ) ,
updatedAt : new Date ( "2026-03-01T00:00:00Z" ) ,
} ,
] ,
archivedAt : null ,
} ,
] ) ;
issueSvc . list . mockResolvedValue ( [
{
id : "issue-1" ,
identifier : "PAP-1" ,
title : "Write launch task" ,
description : "Task body" ,
projectId : "project-1" ,
projectWorkspaceId : "workspace-1" ,
assigneeAgentId : "agent-1" ,
status : "todo" ,
priority : "medium" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : {
mode : "shared_workspace" ,
} ,
assigneeAdapterOverrides : null ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : false ,
projects : true ,
issues : true ,
} ,
} ) ;
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
expect ( extension ) . toContain ( "workspaces:" ) ;
expect ( extension ) . toContain ( "main-repo:" ) ;
expect ( extension ) . toContain ( 'repoUrl: "https://github.com/paperclipai/paperclip.git"' ) ;
expect ( extension ) . toContain ( 'defaultProjectWorkspaceKey: "main-repo"' ) ;
expect ( extension ) . toContain ( 'projectWorkspaceKey: "main-repo"' ) ;
expect ( extension ) . not . toContain ( "/Users/dotta/paperclip" ) ;
expect ( extension ) . not . toContain ( "workspace-1" ) ;
expect ( exported . warnings ) . toContain ( "Project launch workspace Local Scratch was omitted from export because it does not have a portable repoUrl." ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . create . mockResolvedValue ( {
id : "project-imported" ,
name : "Launch" ,
urlKey : "launch" ,
} ) ;
projectSvc . update . mockImplementation ( async ( projectId : string , data : Record < string , unknown > ) = > ( {
id : projectId ,
name : "Launch" ,
urlKey : "launch" ,
. . . data ,
} ) ) ;
projectSvc . createWorkspace . mockImplementation ( async ( projectId : string , data : Record < string , unknown > ) = > ( {
id : "workspace-imported" ,
companyId : "company-imported" ,
projectId ,
name : ` ${ data . name ? ? "Workspace" } ` ,
sourceType : ` ${ data . sourceType ? ? "git_repo" } ` ,
cwd : null ,
repoUrl : typeof data . repoUrl === "string" ? data.repoUrl : null ,
repoRef : typeof data . repoRef === "string" ? data.repoRef : null ,
defaultRef : typeof data . defaultRef === "string" ? data.defaultRef : null ,
visibility : ` ${ data . visibility ? ? "default" } ` ,
setupCommand : typeof data . setupCommand === "string" ? data.setupCommand : null ,
cleanupCommand : typeof data . cleanupCommand === "string" ? data.cleanupCommand : null ,
remoteProvider : null ,
remoteWorkspaceRef : null ,
sharedWorkspaceKey : null ,
metadata : ( data . metadata as Record < string , unknown > | null | undefined ) ? ? null ,
isPrimary : Boolean ( data . isPrimary ) ,
createdAt : new Date ( "2026-03-02T00:00:00Z" ) ,
updatedAt : new Date ( "2026-03-02T00:00:00Z" ) ,
} ) ) ;
issueSvc . create . mockResolvedValue ( {
id : "issue-imported" ,
title : "Write launch task" ,
} ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : false ,
projects : true ,
issues : true ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( projectSvc . createWorkspace ) . toHaveBeenCalledWith ( "project-imported" , expect . objectContaining ( {
name : "Main Repo" ,
sourceType : "git_repo" ,
repoUrl : "https://github.com/paperclipai/paperclip.git" ,
repoRef : "main" ,
defaultRef : "main" ,
visibility : "default" ,
} ) ) ;
expect ( projectSvc . update ) . toHaveBeenCalledWith ( "project-imported" , expect . objectContaining ( {
executionWorkspacePolicy : expect.objectContaining ( {
enabled : true ,
defaultMode : "shared_workspace" ,
defaultProjectWorkspaceId : "workspace-imported" ,
} ) ,
} ) ) ;
expect ( issueSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
projectId : "project-imported" ,
projectWorkspaceId : "workspace-imported" ,
title : "Write launch task" ,
} ) ) ;
} ) ;
2026-03-23 12:35:08 -05:00
it ( "infers portable git metadata from a local checkout without task warning fan-out" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const repoDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-portability-git-" ) ) ;
execFileSync ( "git" , [ "init" ] , { cwd : repoDir , stdio : "ignore" } ) ;
execFileSync ( "git" , [ "checkout" , "-b" , "main" ] , { cwd : repoDir , stdio : "ignore" } ) ;
execFileSync ( "git" , [ "remote" , "add" , "origin" , "https://github.com/paperclipai/paperclip.git" ] , {
cwd : repoDir ,
stdio : "ignore" ,
} ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-1" ,
name : "Paperclip App" ,
urlKey : "paperclip-app" ,
description : "Ship it" ,
leadAgentId : null ,
targetDate : null ,
color : null ,
status : "planned" ,
executionWorkspacePolicy : {
enabled : true ,
defaultMode : "shared_workspace" ,
defaultProjectWorkspaceId : "workspace-1" ,
} ,
workspaces : [
{
id : "workspace-1" ,
companyId : "company-1" ,
projectId : "project-1" ,
name : "paperclip" ,
sourceType : "local_path" ,
cwd : repoDir ,
repoUrl : null ,
repoRef : null ,
defaultRef : null ,
visibility : "default" ,
setupCommand : null ,
cleanupCommand : null ,
remoteProvider : null ,
remoteWorkspaceRef : null ,
sharedWorkspaceKey : null ,
metadata : null ,
isPrimary : true ,
createdAt : new Date ( "2026-03-01T00:00:00Z" ) ,
updatedAt : new Date ( "2026-03-01T00:00:00Z" ) ,
} ,
] ,
archivedAt : null ,
} ,
] ) ;
issueSvc . list . mockResolvedValue ( [
{
id : "issue-1" ,
identifier : "PAP-1" ,
title : "Task one" ,
description : "Task body" ,
projectId : "project-1" ,
projectWorkspaceId : "workspace-1" ,
assigneeAgentId : null ,
status : "todo" ,
priority : "medium" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : false ,
agents : false ,
projects : true ,
issues : true ,
} ,
} ) ;
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
expect ( extension ) . toContain ( 'repoUrl: "https://github.com/paperclipai/paperclip.git"' ) ;
expect ( extension ) . toContain ( 'projectWorkspaceKey: "paperclip"' ) ;
expect ( exported . warnings ) . not . toContainEqual ( expect . stringContaining ( "does not have a portable repoUrl" ) ) ;
expect ( exported . warnings ) . not . toContainEqual ( expect . stringContaining ( "reference workspace workspace-1" ) ) ;
} ) ;
it ( "collapses repeated task workspace warnings into one summary per missing workspace" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-1" ,
name : "Launch" ,
urlKey : "launch" ,
description : "Ship it" ,
leadAgentId : null ,
targetDate : null ,
color : null ,
status : "planned" ,
executionWorkspacePolicy : null ,
workspaces : [
{
id : "workspace-1" ,
companyId : "company-1" ,
projectId : "project-1" ,
name : "Local Scratch" ,
sourceType : "local_path" ,
cwd : "/tmp/local-only" ,
repoUrl : null ,
repoRef : null ,
defaultRef : null ,
visibility : "default" ,
setupCommand : null ,
cleanupCommand : null ,
remoteProvider : null ,
remoteWorkspaceRef : null ,
sharedWorkspaceKey : null ,
metadata : null ,
isPrimary : true ,
createdAt : new Date ( "2026-03-01T00:00:00Z" ) ,
updatedAt : new Date ( "2026-03-01T00:00:00Z" ) ,
} ,
] ,
archivedAt : null ,
} ,
] ) ;
issueSvc . list . mockResolvedValue ( [
{
id : "issue-1" ,
identifier : "PAP-1" ,
title : "Task one" ,
description : null ,
projectId : "project-1" ,
projectWorkspaceId : "workspace-1" ,
assigneeAgentId : null ,
status : "todo" ,
priority : "medium" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
{
id : "issue-2" ,
identifier : "PAP-2" ,
title : "Task two" ,
description : null ,
projectId : "project-1" ,
projectWorkspaceId : "workspace-1" ,
assigneeAgentId : null ,
status : "todo" ,
priority : "medium" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
{
id : "issue-3" ,
identifier : "PAP-3" ,
title : "Task three" ,
description : null ,
projectId : "project-1" ,
projectWorkspaceId : "workspace-1" ,
assigneeAgentId : null ,
status : "todo" ,
priority : "medium" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : false ,
agents : false ,
projects : true ,
issues : true ,
} ,
} ) ;
expect ( exported . warnings ) . toContain ( "Project launch workspace Local Scratch was omitted from export because it does not have a portable repoUrl." ) ;
expect ( exported . warnings ) . toContain ( "Tasks pap-1, pap-2, pap-3 reference workspace workspace-1, but that workspace could not be exported portably." ) ;
expect ( exported . warnings . filter ( ( warning ) = > warning . includes ( "workspace reference workspace-1 was omitted from export" ) ) ) . toHaveLength ( 0 ) ;
expect ( exported . warnings . filter ( ( warning ) = > warning . includes ( "could not be exported portably" ) ) ) . toHaveLength ( 1 ) ;
} ) ;
2026-03-14 09:46:16 -05:00
it ( "reads env inputs back from .paperclip.yaml during preview import" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
const preview = await portability . previewImport ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} ) ;
expect ( preview . errors ) . toEqual ( [ ] ) ;
expect ( preview . envInputs ) . toEqual ( [
{
key : "ANTHROPIC_API_KEY" ,
description : "Provide ANTHROPIC_API_KEY for agent claudecoder" ,
agentSlug : "claudecoder" ,
2026-04-07 06:32:52 -05:00
projectSlug : null ,
2026-03-14 09:46:16 -05:00
kind : "secret" ,
requirement : "optional" ,
defaultValue : "" ,
portability : "portable" ,
} ,
{
key : "GH_TOKEN" ,
description : "Provide GH_TOKEN for agent claudecoder" ,
agentSlug : "claudecoder" ,
2026-04-07 06:32:52 -05:00
projectSlug : null ,
2026-03-14 09:46:16 -05:00
kind : "secret" ,
requirement : "optional" ,
defaultValue : "" ,
portability : "portable" ,
} ,
] ) ;
} ) ;
2026-03-14 18:59:26 -05:00
2026-04-07 06:32:52 -05:00
it ( "exports project env as portable inputs without concrete values" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-1" ,
name : "Launch" ,
urlKey : "launch" ,
description : "Ship it" ,
leadAgentId : "agent-1" ,
targetDate : null ,
color : null ,
status : "planned" ,
env : {
OPENAI_API_KEY : {
type : "plain" ,
value : "sk-project-secret" ,
} ,
DOCS_MODE : {
type : "plain" ,
value : "strict" ,
} ,
GITHUB_TOKEN : {
type : "secret_ref" ,
secretId : "11111111-1111-1111-1111-111111111111" ,
version : "latest" ,
} ,
} ,
executionWorkspacePolicy : null ,
workspaces : [ ] ,
metadata : null ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : false ,
agents : false ,
projects : true ,
issues : false ,
} ,
} ) ;
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
expect ( extension ) . toContain ( "OPENAI_API_KEY:" ) ;
expect ( extension ) . toContain ( "DOCS_MODE:" ) ;
expect ( extension ) . toContain ( "GITHUB_TOKEN:" ) ;
expect ( extension ) . not . toContain ( "sk-project-secret" ) ;
expect ( extension ) . not . toContain ( 'type: "secret_ref"' ) ;
expect ( extension ) . not . toContain ( "11111111-1111-1111-1111-111111111111" ) ;
expect ( extension ) . toContain ( 'default: "strict"' ) ;
expect ( extension ) . toContain ( 'kind: "secret"' ) ;
expect ( extension ) . toContain ( 'kind: "plain"' ) ;
} ) ;
it ( "reads project env inputs back from .paperclip.yaml during preview import" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-1" ,
name : "Launch" ,
urlKey : "launch" ,
description : "Ship it" ,
leadAgentId : "agent-1" ,
targetDate : null ,
color : null ,
status : "planned" ,
env : {
OPENAI_API_KEY : {
type : "plain" ,
value : "sk-project-secret" ,
} ,
} ,
executionWorkspacePolicy : null ,
workspaces : [ ] ,
metadata : null ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : false ,
agents : false ,
projects : true ,
issues : false ,
} ,
} ) ;
const preview = await portability . previewImport ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : false ,
agents : false ,
projects : true ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} ) ;
expect ( preview . errors ) . toEqual ( [ ] ) ;
expect ( preview . envInputs ) . toContainEqual ( {
key : "OPENAI_API_KEY" ,
description : "Optional default for OPENAI_API_KEY on project launch" ,
agentSlug : null ,
projectSlug : "launch" ,
kind : "secret" ,
requirement : "optional" ,
defaultValue : "" ,
portability : "portable" ,
} ) ;
} ) ;
2026-03-23 11:14:01 -05:00
it ( "exports routines as recurring task packages with Paperclip routine extensions" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-1" ,
name : "Launch" ,
urlKey : "launch" ,
description : "Ship it" ,
leadAgentId : "agent-1" ,
targetDate : null ,
color : null ,
status : "planned" ,
executionWorkspacePolicy : null ,
archivedAt : null ,
} ,
] ) ;
routineSvc . list . mockResolvedValue ( [
{
id : "routine-1" ,
companyId : "company-1" ,
projectId : "project-1" ,
goalId : null ,
parentIssueId : null ,
title : "Monday Review" ,
description : "Review pipeline health" ,
assigneeAgentId : "agent-1" ,
priority : "high" ,
status : "paused" ,
concurrencyPolicy : "always_enqueue" ,
catchUpPolicy : "enqueue_missed_with_cap" ,
createdByAgentId : null ,
createdByUserId : null ,
updatedByAgentId : null ,
updatedByUserId : null ,
lastTriggeredAt : null ,
lastEnqueuedAt : null ,
createdAt : new Date ( ) ,
updatedAt : new Date ( ) ,
triggers : [
{
id : "trigger-1" ,
companyId : "company-1" ,
routineId : "routine-1" ,
kind : "schedule" ,
label : "Weekly cadence" ,
enabled : true ,
cronExpression : "0 9 * * 1" ,
timezone : "America/Chicago" ,
nextRunAt : null ,
lastFiredAt : null ,
publicId : "public-1" ,
secretId : "secret-1" ,
signingMode : null ,
replayWindowSec : null ,
lastRotatedAt : null ,
lastResult : null ,
createdByAgentId : null ,
createdByUserId : null ,
updatedByAgentId : null ,
updatedByUserId : null ,
createdAt : new Date ( ) ,
updatedAt : new Date ( ) ,
} ,
{
id : "trigger-2" ,
companyId : "company-1" ,
routineId : "routine-1" ,
kind : "webhook" ,
label : "External nudge" ,
enabled : false ,
cronExpression : null ,
timezone : null ,
nextRunAt : null ,
lastFiredAt : null ,
publicId : "public-2" ,
secretId : "secret-2" ,
signingMode : "hmac_sha256" ,
replayWindowSec : 120 ,
lastRotatedAt : null ,
lastResult : null ,
createdByAgentId : null ,
createdByUserId : null ,
updatedByAgentId : null ,
updatedByUserId : null ,
createdAt : new Date ( ) ,
updatedAt : new Date ( ) ,
} ,
] ,
lastRun : null ,
activeIssue : null ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : true ,
issues : true ,
skills : false ,
} ,
} ) ;
expect ( asTextFile ( exported . files [ "tasks/monday-review/TASK.md" ] ) ) . toContain ( 'recurring: true' ) ;
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
expect ( extension ) . toContain ( "routines:" ) ;
expect ( extension ) . toContain ( "monday-review:" ) ;
expect ( extension ) . toContain ( 'cronExpression: "0 9 * * 1"' ) ;
expect ( extension ) . toContain ( 'signingMode: "hmac_sha256"' ) ;
expect ( extension ) . not . toContain ( "secretId" ) ;
expect ( extension ) . not . toContain ( "publicId" ) ;
expect ( exported . manifest . issues ) . toEqual ( [
expect . objectContaining ( {
slug : "monday-review" ,
recurring : true ,
status : "paused" ,
priority : "high" ,
routine : expect.objectContaining ( {
concurrencyPolicy : "always_enqueue" ,
catchUpPolicy : "enqueue_missed_with_cap" ,
triggers : expect.arrayContaining ( [
expect . objectContaining ( { kind : "schedule" , cronExpression : "0 9 * * 1" , timezone : "America/Chicago" } ) ,
expect . objectContaining ( { kind : "webhook" , enabled : false , signingMode : "hmac_sha256" , replayWindowSec : 120 } ) ,
] ) ,
} ) ,
} ) ,
] ) ;
} ) ;
it ( "imports recurring task packages as routines instead of one-time issues" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-created" ,
name : "ClaudeCoder" ,
} ) ;
projectSvc . create . mockResolvedValue ( {
id : "project-created" ,
name : "Launch" ,
urlKey : "launch" ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
const files = {
"COMPANY.md" : [
"---" ,
'schema: "agentcompanies/v1"' ,
'name: "Imported Paperclip"' ,
"---" ,
"" ,
] . join ( "\n" ) ,
"agents/claudecoder/AGENTS.md" : [
"---" ,
'name: "ClaudeCoder"' ,
"---" ,
"" ,
"You write code." ,
"" ,
] . join ( "\n" ) ,
"projects/launch/PROJECT.md" : [
"---" ,
'name: "Launch"' ,
"---" ,
"" ,
] . join ( "\n" ) ,
"tasks/monday-review/TASK.md" : [
"---" ,
'name: "Monday Review"' ,
'project: "launch"' ,
'assignee: "claudecoder"' ,
"recurring: true" ,
"---" ,
"" ,
"Review pipeline health." ,
"" ,
] . join ( "\n" ) ,
".paperclip.yaml" : [
'schema: "paperclip/v1"' ,
"routines:" ,
" monday-review:" ,
' status: "paused"' ,
' priority: "high"' ,
' concurrencyPolicy: "always_enqueue"' ,
' catchUpPolicy: "enqueue_missed_with_cap"' ,
" triggers:" ,
" - kind: schedule" ,
' cronExpression: "0 9 * * 1"' ,
' timezone: "America/Chicago"' ,
' - kind: webhook' ,
' enabled: false' ,
' signingMode: "hmac_sha256"' ,
' replayWindowSec: 120' ,
"" ,
] . join ( "\n" ) ,
} ;
const preview = await portability . previewImport ( {
source : { type : "inline" , rootPath : "paperclip-demo" , files } ,
include : { company : true , agents : true , projects : true , issues : true , skills : false } ,
target : { mode : "new_company" , newCompanyName : "Imported Paperclip" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} ) ;
expect ( preview . errors ) . toEqual ( [ ] ) ;
expect ( preview . plan . issuePlans ) . toEqual ( [
expect . objectContaining ( {
slug : "monday-review" ,
reason : "Recurring task will be imported as a routine." ,
} ) ,
] ) ;
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
const result = await portability . importBundle ( {
2026-03-23 11:14:01 -05:00
source : { type : "inline" , rootPath : "paperclip-demo" , files } ,
include : { company : true , agents : true , projects : true , issues : true , skills : false } ,
target : { mode : "new_company" , newCompanyName : "Imported Paperclip" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( routineSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
projectId : "project-created" ,
title : "Monday Review" ,
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:19:48 -05:00
assigneeAgentId : "agent-created" ,
2026-03-23 11:14:01 -05:00
priority : "high" ,
status : "paused" ,
concurrencyPolicy : "always_enqueue" ,
catchUpPolicy : "enqueue_missed_with_cap" ,
} ) , expect . any ( Object ) ) ;
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:19:48 -05:00
expect ( result . warnings ) . not . toContain (
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
"Task monday-review assignee claudecoder is pending_approval; imported work was left unassigned." ,
) ;
2026-03-23 11:14:01 -05:00
expect ( routineSvc . createTrigger ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( routineSvc . createTrigger ) . toHaveBeenCalledWith ( "routine-created" , expect . objectContaining ( {
kind : "schedule" ,
cronExpression : "0 9 * * 1" ,
timezone : "America/Chicago" ,
} ) , expect . any ( Object ) ) ;
expect ( routineSvc . createTrigger ) . toHaveBeenCalledWith ( "routine-created" , expect . objectContaining ( {
kind : "webhook" ,
enabled : false ,
signingMode : "hmac_sha256" ,
replayWindowSec : 120 ,
} ) , expect . any ( Object ) ) ;
expect ( issueSvc . create ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( "migrates legacy schedule.recurrence imports into routine triggers" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-created" ,
name : "ClaudeCoder" ,
} ) ;
projectSvc . create . mockResolvedValue ( {
id : "project-created" ,
name : "Launch" ,
urlKey : "launch" ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
const files = {
"COMPANY.md" : [ '---' , 'schema: "agentcompanies/v1"' , 'name: "Imported Paperclip"' , "---" , "" ] . join ( "\n" ) ,
"agents/claudecoder/AGENTS.md" : [ '---' , 'name: "ClaudeCoder"' , "---" , "" , "You write code." , "" ] . join ( "\n" ) ,
"projects/launch/PROJECT.md" : [ '---' , 'name: "Launch"' , "---" , "" ] . join ( "\n" ) ,
"tasks/monday-review/TASK.md" : [
"---" ,
'name: "Monday Review"' ,
'project: "launch"' ,
'assignee: "claudecoder"' ,
"schedule:" ,
' timezone: "America/Chicago"' ,
' startsAt: "2026-03-16T09:00:00-05:00"' ,
" recurrence:" ,
' frequency: "weekly"' ,
" interval: 1" ,
" weekdays:" ,
' - "monday"' ,
"---" ,
"" ,
"Review pipeline health." ,
"" ,
] . join ( "\n" ) ,
} ;
const preview = await portability . previewImport ( {
source : { type : "inline" , rootPath : "paperclip-demo" , files } ,
include : { company : true , agents : true , projects : true , issues : true , skills : false } ,
target : { mode : "new_company" , newCompanyName : "Imported Paperclip" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} ) ;
expect ( preview . errors ) . toEqual ( [ ] ) ;
expect ( preview . manifest . issues [ 0 ] ) . toEqual ( expect . objectContaining ( {
recurring : true ,
legacyRecurrence : expect.objectContaining ( { frequency : "weekly" } ) ,
} ) ) ;
await portability . importBundle ( {
source : { type : "inline" , rootPath : "paperclip-demo" , files } ,
include : { company : true , agents : true , projects : true , issues : true , skills : false } ,
target : { mode : "new_company" , newCompanyName : "Imported Paperclip" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( routineSvc . createTrigger ) . toHaveBeenCalledWith ( "routine-created" , expect . objectContaining ( {
kind : "schedule" ,
cronExpression : "0 9 * * 1" ,
timezone : "America/Chicago" ,
} ) , expect . any ( Object ) ) ;
expect ( issueSvc . create ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( "flags recurring task imports that are missing routine-required fields" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const preview = await portability . previewImport ( {
source : {
type : "inline" ,
rootPath : "paperclip-demo" ,
files : {
"COMPANY.md" : [ '---' , 'schema: "agentcompanies/v1"' , 'name: "Imported Paperclip"' , "---" , "" ] . join ( "\n" ) ,
"tasks/monday-review/TASK.md" : [
"---" ,
'name: "Monday Review"' ,
"recurring: true" ,
"---" ,
"" ,
"Review pipeline health." ,
"" ,
] . join ( "\n" ) ,
} ,
} ,
include : { company : true , agents : false , projects : false , issues : true , skills : false } ,
target : { mode : "new_company" , newCompanyName : "Imported Paperclip" } ,
collisionStrategy : "rename" ,
} ) ;
expect ( preview . errors ) . toContain ( "Recurring task monday-review must declare a project to import as a routine." ) ;
expect ( preview . errors ) . toContain ( "Recurring task monday-review must declare an assignee to import as a routine." ) ;
} ) ;
2026-03-20 14:06:37 -05:00
it ( "imports a vendor-neutral package without .paperclip.yaml" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-created" ,
name : "ClaudeCoder" ,
} ) ;
const preview = await portability . previewImport ( {
source : {
type : "inline" ,
rootPath : "paperclip-demo" ,
files : {
"COMPANY.md" : [
"---" ,
'schema: "agentcompanies/v1"' ,
'name: "Imported Paperclip"' ,
'description: "Portable company package"' ,
"---" ,
"" ,
"# Imported Paperclip" ,
"" ,
] . join ( "\n" ) ,
"agents/claudecoder/AGENTS.md" : [
"---" ,
'name: "ClaudeCoder"' ,
'title: "Software Engineer"' ,
"---" ,
"" ,
"# ClaudeCoder" ,
"" ,
"You write code." ,
"" ,
] . join ( "\n" ) ,
} ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} ) ;
expect ( preview . errors ) . toEqual ( [ ] ) ;
expect ( preview . manifest . company ? . name ) . toBe ( "Imported Paperclip" ) ;
expect ( preview . manifest . agents ) . toEqual ( [
expect . objectContaining ( {
slug : "claudecoder" ,
name : "ClaudeCoder" ,
adapterType : "process" ,
} ) ,
] ) ;
expect ( preview . envInputs ) . toEqual ( [ ] ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : "paperclip-demo" ,
files : {
"COMPANY.md" : [
"---" ,
'schema: "agentcompanies/v1"' ,
'name: "Imported Paperclip"' ,
'description: "Portable company package"' ,
"---" ,
"" ,
"# Imported Paperclip" ,
"" ,
] . join ( "\n" ) ,
"agents/claudecoder/AGENTS.md" : [
"---" ,
'name: "ClaudeCoder"' ,
'title: "Software Engineer"' ,
"---" ,
"" ,
"# ClaudeCoder" ,
"" ,
"You write code." ,
"" ,
] . join ( "\n" ) ,
} ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( companySvc . create ) . toHaveBeenCalledWith ( expect . objectContaining ( {
name : "Imported Paperclip" ,
description : "Portable company package" ,
} ) ) ;
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
name : "ClaudeCoder" ,
adapterType : "process" ,
} ) ) ;
} ) ;
2026-04-03 23:24:24 +09:00
it ( "preserves agent role from frontmatter when extension block omits it" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const preview = await portability . previewImport ( {
source : {
type : "inline" ,
rootPath : "ceo-package" ,
files : {
"COMPANY.md" : [
"---" ,
'schema: "agentcompanies/v1"' ,
'name: "CEO Role Test"' ,
"---" ,
"" ,
] . join ( "\n" ) ,
"agents/ceo/AGENTS.md" : [
"---" ,
'name: "CEO"' ,
'role: "ceo"' ,
"---" ,
"" ,
"# CEO" ,
"" ,
"You run the company." ,
"" ,
] . join ( "\n" ) ,
} ,
} ,
include : { company : true , agents : true , projects : false , issues : false } ,
target : { mode : "new_company" , newCompanyName : "CEO Role Test" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} ) ;
expect ( preview . errors ) . toEqual ( [ ] ) ;
expect ( preview . manifest . agents ) . toEqual ( [
expect . objectContaining ( {
slug : "ceo" ,
name : "CEO" ,
role : "ceo" ,
} ) ,
] ) ;
} ) ;
2026-03-20 08:55:10 -05:00
it ( "treats no-separator auth and api key env names as secrets during export" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
agentSvc . list . mockResolvedValue ( [
{
id : "agent-1" ,
name : "ClaudeCoder" ,
status : "idle" ,
role : "engineer" ,
title : "Software Engineer" ,
icon : "code" ,
reportsTo : null ,
capabilities : "Writes code" ,
adapterType : "claude_local" ,
adapterConfig : {
promptTemplate : "You are ClaudeCoder." ,
env : {
APIKEY : {
type : "plain" ,
value : "sk-plain-api" ,
} ,
GITHUBAUTH : {
type : "plain" ,
value : "gh-auth-token" ,
} ,
PRIVATEKEY : {
type : "plain" ,
value : "private-key-value" ,
} ,
} ,
} ,
runtimeConfig : { } ,
budgetMonthlyCents : 0 ,
permissions : { } ,
metadata : null ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
expect ( extension ) . toContain ( "APIKEY:" ) ;
expect ( extension ) . toContain ( "GITHUBAUTH:" ) ;
expect ( extension ) . toContain ( "PRIVATEKEY:" ) ;
expect ( extension ) . not . toContain ( "sk-plain-api" ) ;
expect ( extension ) . not . toContain ( "gh-auth-token" ) ;
expect ( extension ) . not . toContain ( "private-key-value" ) ;
expect ( extension ) . toContain ( 'kind: "secret"' ) ;
} ) ;
2026-03-14 18:59:26 -05:00
it ( "imports packaged skills and restores desired skill refs on agents" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-created" ,
name : "ClaudeCoder" ,
} ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
2026-03-20 06:20:30 -05:00
const textOnlyFiles = Object . fromEntries ( Object . entries ( exported . files ) . filter ( ( [ , v ] ) = > typeof v === "string" ) ) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith ( "company-imported" , textOnlyFiles , {
2026-03-18 21:54:10 -05:00
onConflict : "replace" ,
} ) ;
2026-03-14 18:59:26 -05:00
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
adapterConfig : expect.objectContaining ( {
paperclipSkillSync : {
2026-03-16 18:27:20 -05:00
desiredSkills : [ paperclipKey ] ,
2026-03-14 18:59:26 -05:00
} ,
} ) ,
} ) ) ;
} ) ;
2026-03-16 10:14:09 -05:00
2026-03-19 07:24:04 -05:00
it ( "imports a packaged company logo and attaches it to the target company" , async ( ) = > {
const storage = {
putFile : vi.fn ( ) . mockResolvedValue ( {
provider : "local_disk" ,
objectKey : "assets/companies/imported-logo" ,
contentType : "image/png" ,
byteSize : 9 ,
sha256 : "logo-sha" ,
originalFilename : "company-logo.png" ,
} ) ,
} ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
logoAssetId : null ,
} ) ;
companySvc . update . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
logoAssetId : "asset-created" ,
} ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-created" ,
name : "ClaudeCoder" ,
} ) ;
const portability = companyPortabilityService ( { } as any , storage as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
exported . files [ "images/company-logo.png" ] = {
encoding : "base64" ,
data : Buffer.from ( "png-bytes" ) . toString ( "base64" ) ,
contentType : "image/png" ,
} ;
exported . files [ ".paperclip.yaml" ] = ` ${ exported . files [ ".paperclip.yaml" ] } ` . replace (
'brandColor: "#5c5fff"\n' ,
'brandColor: "#5c5fff"\n logoPath: "images/company-logo.png"\n' ,
) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( storage . putFile ) . toHaveBeenCalledWith ( expect . objectContaining ( {
companyId : "company-imported" ,
namespace : "assets/companies" ,
originalFilename : "company-logo.png" ,
contentType : "image/png" ,
body : Buffer.from ( "png-bytes" ) ,
} ) ) ;
expect ( assetSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
objectKey : "assets/companies/imported-logo" ,
contentType : "image/png" ,
createdByUserId : "user-1" ,
} ) ) ;
expect ( companySvc . update ) . toHaveBeenCalledWith ( "company-imported" , {
logoAssetId : "asset-created" ,
} ) ;
} ) ;
2026-03-18 21:54:10 -05:00
it ( "copies source company memberships for safe new-company imports" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-created" ,
name : "ClaudeCoder" ,
} ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} , null , {
mode : "agent_safe" ,
sourceCompanyId : "company-1" ,
} ) ;
expect ( accessSvc . listActiveUserMemberships ) . toHaveBeenCalledWith ( "company-1" ) ;
expect ( accessSvc . copyActiveUserMemberships ) . toHaveBeenCalledWith ( "company-1" , "company-imported" ) ;
expect ( accessSvc . ensureMembership ) . not . toHaveBeenCalledWith ( "company-imported" , "user" , expect . anything ( ) , "owner" , "active" ) ;
2026-03-20 06:20:30 -05:00
const textOnlyFiles = Object . fromEntries ( Object . entries ( exported . files ) . filter ( ( [ , v ] ) = > typeof v === "string" ) ) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith ( "company-imported" , textOnlyFiles , {
2026-03-18 21:54:10 -05:00
onConflict : "rename" ,
} ) ;
} ) ;
2026-03-23 16:30:28 -05:00
it ( "disables timer heartbeats on imported agents" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
agentSvc . create . mockImplementation ( async ( _companyId : string , input : Record < string , unknown > ) = > ( {
id : ` agent- ${ String ( input . name ) . toLowerCase ( ) } ` ,
name : input.name ,
adapterConfig : input.adapterConfig ,
runtimeConfig : input.runtimeConfig ,
} ) ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
const createdClaude = agentSvc . create . mock . calls . find ( ( [ , input ] ) = > input . name === "ClaudeCoder" ) ;
expect ( createdClaude ? . [ 1 ] ) . toMatchObject ( {
runtimeConfig : {
heartbeat : {
enabled : false ,
2026-05-01 10:42:56 -05:00
maxConcurrentRuns : 20 ,
2026-03-23 16:30:28 -05:00
} ,
} ,
} ) ;
} ) ;
2026-03-16 10:14:09 -05:00
it ( "imports only selected files and leaves unchecked company metadata alone" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
companySvc . getById . mockResolvedValue ( {
id : "company-1" ,
name : "Paperclip" ,
description : "Existing company" ,
brandColor : "#123456" ,
requireBoardApprovalForNewAgents : false ,
} ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-cmo" ,
name : "CMO" ,
} ) ;
const result = await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : true ,
issues : true ,
} ,
selectedFiles : [ "agents/cmo/AGENTS.md" ] ,
target : {
mode : "existing_company" ,
companyId : "company-1" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( companySvc . update ) . not . toHaveBeenCalled ( ) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith (
"company-1" ,
expect . objectContaining ( {
"COMPANY.md" : expect . any ( String ) ,
"agents/cmo/AGENTS.md" : expect . any ( String ) ,
} ) ,
2026-03-18 21:54:10 -05:00
{
onConflict : "replace" ,
} ,
2026-03-16 10:14:09 -05:00
) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith (
"company-1" ,
expect . not . objectContaining ( {
"agents/claudecoder/AGENTS.md" : expect . any ( String ) ,
} ) ,
2026-03-18 21:54:10 -05:00
{
onConflict : "replace" ,
} ,
2026-03-16 10:14:09 -05:00
) ;
expect ( agentSvc . create ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-1" , expect . objectContaining ( {
name : "CMO" ,
2026-03-23 16:30:28 -05:00
runtimeConfig : {
heartbeat : {
enabled : false ,
2026-05-01 10:42:56 -05:00
maxConcurrentRuns : 20 ,
2026-03-23 16:30:28 -05:00
} ,
} ,
2026-03-16 10:14:09 -05:00
} ) ) ;
expect ( result . company . action ) . toBe ( "unchanged" ) ;
expect ( result . agents ) . toEqual ( [
{
slug : "cmo" ,
id : "agent-cmo" ,
action : "created" ,
name : "CMO" ,
reason : null ,
} ,
] ) ;
} ) ;
2026-03-16 12:17:28 -05:00
it ( "applies adapter overrides while keeping imported AGENTS content implicit" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-created" ,
name : "ClaudeCoder" ,
} ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : "all" ,
collisionStrategy : "rename" ,
adapterOverrides : {
claudecoder : {
adapterType : "codex_local" ,
adapterConfig : {
dangerouslyBypassApprovalsAndSandbox : true ,
instructionsFilePath : "/tmp/should-not-survive.md" ,
} ,
} ,
} ,
} , "user-1" ) ;
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
adapterType : "codex_local" ,
adapterConfig : expect.objectContaining ( {
dangerouslyBypassApprovalsAndSandbox : true ,
} ) ,
} ) ) ;
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
adapterConfig : expect.not.objectContaining ( {
instructionsFilePath : expect.anything ( ) ,
2026-03-17 13:42:00 -05:00
promptTemplate : expect.anything ( ) ,
2026-03-16 12:17:28 -05:00
} ) ,
} ) ) ;
2026-03-17 13:42:00 -05:00
expect ( agentInstructionsSvc . materializeManagedBundle ) . toHaveBeenCalledWith (
expect . objectContaining ( { name : "ClaudeCoder" } ) ,
expect . objectContaining ( {
"AGENTS.md" : expect . stringContaining ( "You are ClaudeCoder." ) ,
} ) ,
expect . objectContaining ( {
clearLegacyPromptTemplate : true ,
replaceExisting : true ,
} ) ,
) ;
2026-03-23 20:04:40 -05:00
const materializedFiles = agentInstructionsSvc . materializeManagedBundle . mock . calls [ 0 ] ? . [ 1 ] as Record < string , string > ;
expect ( materializedFiles [ "AGENTS.md" ] ) . not . toMatch ( /^---\n/ ) ;
expect ( materializedFiles [ "AGENTS.md" ] ) . not . toContain ( 'name: "ClaudeCoder"' ) ;
2026-03-16 12:17:28 -05:00
} ) ;
2026-03-23 20:56:34 -05:00
2026-05-06 07:49:47 -05:00
it ( "does not implicitly add local adapter permission bypass defaults on import" , async ( ) = > {
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . create . mockImplementation ( async ( _companyId : string , input : Record < string , unknown > ) = > ( {
id : "agent-created" ,
name : String ( input . name ) ,
adapterType : input.adapterType ,
adapterConfig : input.adapterConfig ,
} ) ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : [ "claudecoder" ] ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
2026-05-06 07:49:47 -05:00
// Imports must preserve safe-by-default local adapter settings unless the package says otherwise.
const firstCreateInput = agentSvc . create . mock . calls [ 0 ] ? . [ 1 ] as Record < string , any > ;
expect ( firstCreateInput ? . adapterConfig ) . toBeTruthy ( ) ;
expect ( firstCreateInput . adapterConfig ? . dangerouslySkipPermissions ) . toBeUndefined ( ) ;
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : [ "claudecoder" ] ,
collisionStrategy : "rename" ,
adapterOverrides : {
claudecoder : {
adapterType : "codex_local" ,
adapterConfig : {
extraArgs : [ ] ,
args : [ "--legacy-arg" ] ,
} ,
} ,
} ,
} , "user-1" ) ;
expect ( agentSvc . create ) . toHaveBeenLastCalledWith ( "company-imported" , expect . objectContaining ( {
adapterType : "codex_local" ,
adapterConfig : expect.objectContaining ( {
extraArgs : [ "--skip-git-repo-check" ] ,
args : [ "--legacy-arg" ] ,
} ) ,
} ) ) ;
2026-05-06 07:49:47 -05:00
const lastCreateInput = agentSvc . create . mock . calls . at ( - 1 ) ? . [ 1 ] as Record < string , any > ;
expect ( lastCreateInput ? . adapterConfig ) . toBeTruthy ( ) ;
expect ( lastCreateInput . adapterConfig ? . dangerouslyBypassApprovalsAndSandbox ) . toBeUndefined ( ) ;
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
} ) ;
[codex] Harden execution reliability and heartbeat tooling (#3679)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Reliable execution depends on heartbeat routing, issue lifecycle
semantics, telemetry, and a fast enough local verification loop to keep
regressions visible
> - The remaining commits on this branch were mostly server/runtime
correctness fixes plus test and documentation follow-ups in that area
> - Those changes are logically separate from the UI-focused
issue-detail and workspace/navigation branches even when they touch
overlapping issue APIs
> - This pull request groups the execution reliability, heartbeat,
telemetry, and tooling changes into one standalone branch
> - The benefit is a focused review of the control-plane correctness
work, including the follow-up fix that restored the implicit
comment-reopen helpers after branch splitting
## What Changed
- Hardened issue/heartbeat execution behavior, including self-review
stage skipping, deferred mention wakes during active execution, stranded
execution recovery, active-run scoping, assignee resolution, and
blocked-to-todo wake resumption
- Reduced noisy polling/logging overhead by trimming issue run payloads,
compacting persisted run logs, silencing high-volume request logs, and
capping heartbeat-run queries in dashboard/inbox surfaces
- Expanded telemetry and status semantics with adapter/model fields on
task completion plus clearer status guidance in docs/onboarding material
- Updated test infrastructure and verification defaults with faster
route-test module isolation, cheaper default `pnpm test`, e2e isolation
from local state, and repo verification follow-ups
- Included docs/release housekeeping from the branch and added a small
follow-up commit restoring the implicit comment-reopen helpers that were
dropped during branch reconstruction
## Verification
- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts`
- `pnpm vitest run server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/health.test.ts`
- `server/src/__tests__/activity-service.test.ts`,
`server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and
`server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted
on this host but the embedded Postgres harness reported
init-script/data-dir problems and skipped or failed to start, so they
are noted as environment-limited
## Risks
- Medium: this branch changes core issue/heartbeat routing and
reopen/wakeup behavior, so regressions would affect agent execution flow
rather than isolated UI polish
- Because it also updates verification infrastructure, reviewers should
pay attention to whether the new tests are asserting the right failure
modes and not just reshaping harness behavior
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 13:34:52 -05:00
it ( "preserves issue labelIds through export and import round-trip" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [
{
id : "project-1" ,
name : "Launch" ,
urlKey : "launch" ,
description : null ,
status : "active" ,
leadAgentId : null ,
metadata : null ,
defaultProjectWorkspaceId : null ,
} ,
] ) ;
projectSvc . listWorkspaces . mockResolvedValue ( [ ] ) ;
issueSvc . list . mockResolvedValue ( [
{
id : "issue-1" ,
identifier : "PAP-1" ,
title : "Labelled task" ,
description : "Has labels" ,
projectId : "project-1" ,
projectWorkspaceId : null ,
assigneeAgentId : null ,
status : "todo" ,
priority : "high" ,
labelIds : [ "label-a" , "label-b" ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : { company : true , agents : false , projects : true , issues : true } ,
} ) ;
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
expect ( extension ) . toContain ( "labelIds:" ) ;
expect ( extension ) . toContain ( "label-a" ) ;
expect ( extension ) . toContain ( "label-b" ) ;
companySvc . create . mockResolvedValue ( { id : "company-imported" , name : "Imported" } ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . create . mockResolvedValue ( { id : "project-imported" , name : "Launch" , urlKey : "launch" } ) ;
issueSvc . create . mockResolvedValue ( { id : "issue-imported" , title : "Labelled task" } ) ;
await portability . importBundle ( {
source : { type : "inline" , rootPath : exported.rootPath , files : exported.files } ,
include : { company : true , agents : false , projects : true , issues : true } ,
target : { mode : "new_company" , newCompanyName : "Imported" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( issueSvc . create ) . toHaveBeenCalledWith (
"company-imported" ,
expect . objectContaining ( {
labelIds : [ "label-a" , "label-b" ] ,
} ) ,
) ;
} ) ;
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
it ( "preserves issue comment presentation fields through export and import" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const presentation = { kind : "system_notice" , tone : "warning" , detailsDefaultOpen : false } ;
const metadata = {
version : 1 ,
sections : [ { rows : [ { type : "key_value" , label : "Cause" , value : "successful_run_missing_state" } ] } ] ,
} ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . listWorkspaces . mockResolvedValue ( [ ] ) ;
issueSvc . list . mockResolvedValue ( [
{
id : "issue-1" ,
identifier : "PAP-1" ,
title : "Needs disposition" ,
description : "System notice source" ,
projectId : null ,
projectWorkspaceId : null ,
assigneeAgentId : null ,
status : "todo" ,
priority : "high" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
] ) ;
issueSvc . listComments . mockResolvedValue ( [
{
id : "comment-1" ,
issueId : "issue-1" ,
companyId : "company-1" ,
authorType : "system" ,
authorAgentId : null ,
authorUserId : null ,
body : "Paperclip needs a disposition before this issue can continue." ,
presentation ,
metadata ,
createdAt : new Date ( "2026-05-04T12:00:00.000Z" ) ,
updatedAt : new Date ( "2026-05-04T12:00:00.000Z" ) ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : { company : true , agents : false , projects : false , issues : true } ,
} ) ;
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
expect ( extension ) . toContain ( "comments:" ) ;
expect ( extension ) . toContain ( "system_notice" ) ;
expect ( extension ) . toContain ( "successful_run_missing_state" ) ;
companySvc . create . mockResolvedValue ( { id : "company-imported" , name : "Imported" } ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
issueSvc . create . mockResolvedValue ( { id : "issue-imported" , title : "Needs disposition" } ) ;
await portability . importBundle ( {
source : { type : "inline" , rootPath : exported.rootPath , files : exported.files } ,
include : { company : true , agents : false , projects : false , issues : true } ,
target : { mode : "new_company" , newCompanyName : "Imported" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( issueSvc . addComment ) . toHaveBeenCalledWith (
"issue-imported" ,
"Paperclip needs a disposition before this issue can continue." ,
{ agentId : undefined , userId : undefined } ,
{
authorType : "system" ,
presentation ,
metadata ,
createdAt : "2026-05-04T12:00:00.000Z" ,
} ,
) ;
} ) ;
2026-05-06 07:49:47 -05:00
it ( "does not export raw comment author user ids" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . listWorkspaces . mockResolvedValue ( [ ] ) ;
issueSvc . list . mockResolvedValue ( [
{
id : "issue-1" ,
identifier : "PAP-1" ,
title : "Private board note" ,
description : null ,
projectId : null ,
projectWorkspaceId : null ,
assigneeAgentId : null ,
status : "todo" ,
priority : "medium" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
] ) ;
issueSvc . listComments . mockResolvedValue ( [
{
id : "comment-1" ,
issueId : "issue-1" ,
companyId : "company-1" ,
authorType : "user" ,
authorAgentId : null ,
authorUserId : "local-board" ,
body : "Need private follow-up." ,
presentation : null ,
metadata : null ,
createdAt : new Date ( "2026-05-04T12:00:00.000Z" ) ,
updatedAt : new Date ( "2026-05-04T12:00:00.000Z" ) ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : { company : true , agents : false , projects : false , issues : true } ,
} ) ;
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
expect ( extension ) . toContain ( 'authorType: "user"' ) ;
expect ( extension ) . not . toContain ( "authorUserId: local-board" ) ;
} ) ;
it ( "downgrades user-authored imported comments to system when no importing user exists" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . listWorkspaces . mockResolvedValue ( [ ] ) ;
issueSvc . list . mockResolvedValue ( [
{
id : "issue-1" ,
identifier : "PAP-1" ,
title : "Private board note" ,
description : null ,
projectId : null ,
projectWorkspaceId : null ,
assigneeAgentId : null ,
status : "todo" ,
priority : "medium" ,
labelIds : [ ] ,
billingCode : null ,
executionWorkspaceSettings : null ,
assigneeAdapterOverrides : null ,
} ,
] ) ;
issueSvc . listComments . mockResolvedValue ( [
{
id : "comment-1" ,
issueId : "issue-1" ,
companyId : "company-1" ,
authorType : "user" ,
authorAgentId : null ,
authorUserId : "local-board" ,
body : "Need private follow-up." ,
presentation : null ,
metadata : null ,
createdAt : new Date ( "2026-05-04T12:00:00.000Z" ) ,
updatedAt : new Date ( "2026-05-04T12:00:00.000Z" ) ,
} ,
] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : { company : true , agents : false , projects : false , issues : true } ,
} ) ;
companySvc . create . mockResolvedValue ( { id : "company-imported" , name : "Imported" } ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
issueSvc . create . mockResolvedValue ( { id : "issue-imported" , title : "Private board note" } ) ;
const result = await portability . importBundle ( {
source : { type : "inline" , rootPath : exported.rootPath , files : exported.files } ,
include : { company : true , agents : false , projects : false , issues : true } ,
target : { mode : "new_company" , newCompanyName : "Imported" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} , null ) ;
expect ( issueSvc . addComment ) . toHaveBeenCalledWith (
"issue-imported" ,
"Need private follow-up." ,
{ agentId : undefined , userId : undefined } ,
{
authorType : "system" ,
presentation : null ,
metadata : null ,
createdAt : "2026-05-04T12:00:00.000Z" ,
} ,
) ;
expect ( result . warnings ) . toContain (
"Comment on task pap-1 was imported as a system comment because no importing user was available." ,
) ;
} ) ;
2026-03-23 20:56:34 -05:00
it ( "strips root AGENTS frontmatter when importing a nested agent entry path" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
companySvc . create . mockResolvedValue ( {
id : "company-imported" ,
name : "Imported Paperclip" ,
} ) ;
accessSvc . ensureMembership . mockResolvedValue ( undefined ) ;
agentSvc . create . mockResolvedValue ( {
id : "agent-created" ,
name : "ClaudeCoder" ,
} ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
const originalAgentsMarkdown = exported . files [ "agents/claudecoder/AGENTS.md" ] ;
expect ( typeof originalAgentsMarkdown ) . toBe ( "string" ) ;
const files = {
. . . exported . files ,
"agents/claudecoder/nested/AGENTS.md" : originalAgentsMarkdown ! ,
} ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : [ "claudecoder" ] ,
collisionStrategy : "rename" ,
adapterOverrides : {
claudecoder : {
adapterType : "codex_local" ,
adapterConfig : {
dangerouslyBypassApprovalsAndSandbox : true ,
} ,
} ,
} ,
} , "user-1" ) ;
const nestedMaterializedFiles = agentInstructionsSvc . materializeManagedBundle . mock . calls
. map ( ( [ , filesArg ] ) = > filesArg as Record < string , string > )
. find ( ( filesArg ) = > typeof filesArg [ "nested/AGENTS.md" ] === "string" ) ;
expect ( nestedMaterializedFiles ) . toBeDefined ( ) ;
expect ( nestedMaterializedFiles ? . [ "nested/AGENTS.md" ] ) . toContain ( "You are ClaudeCoder." ) ;
expect ( nestedMaterializedFiles ? . [ "AGENTS.md" ] ) . toContain ( "You are ClaudeCoder." ) ;
expect ( nestedMaterializedFiles ? . [ "AGENTS.md" ] ) . not . toMatch ( /^---\n/ ) ;
expect ( nestedMaterializedFiles ? . [ "AGENTS.md" ] ) . not . toContain ( 'name: "ClaudeCoder"' ) ;
} ) ;
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
it ( "rejects dangerous adapter types on agent-safe imports" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
await expect ( portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : false ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "existing_company" ,
companyId : "company-1" ,
} ,
agents : [ "claudecoder" ] ,
collisionStrategy : "rename" ,
adapterOverrides : {
claudecoder : {
adapterType : "process" ,
adapterConfig : {
command : "/bin/sh" ,
args : [ "-c" , "id" ] ,
} ,
} ,
} ,
} , "user-1" , {
mode : "agent_safe" ,
sourceCompanyId : "company-1" ,
} ) ) . rejects . toThrow ( 'Adapter type "process" is not allowed in safe imports' ) ;
expect ( agentSvc . create ) . not . toHaveBeenCalled ( ) ;
} ) ;
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:19:48 -05:00
it ( "imports new agents as active while preserving future hire approval settings" , async ( ) = > {
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
secretSvc . normalizeAdapterConfigForPersistence . mockResolvedValueOnce ( {
normalized : true ,
env : {
OPENAI_API_KEY : {
type : "secret_ref" ,
secretId : "secret-1" ,
version : "latest" ,
} ,
} ,
} ) ;
agentSvc . create . mockImplementation ( async ( _companyId : string , input : Record < string , unknown > ) = > ( {
id : "agent-created" ,
name : String ( input . name ) ,
adapterType : input.adapterType ,
adapterConfig : input.adapterConfig ,
status : input.status ,
} ) ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "new_company" ,
newCompanyName : "Imported Paperclip" ,
} ,
agents : [ "claudecoder" ] ,
collisionStrategy : "rename" ,
} , "user-1" ) ;
expect ( secretSvc . normalizeAdapterConfigForPersistence ) . toHaveBeenCalledWith (
"company-imported" ,
2026-05-06 07:49:47 -05:00
expect . anything ( ) ,
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
{ strictMode : false } ,
) ;
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
adapterType : "claude_local" ,
adapterConfig : expect.objectContaining ( {
normalized : true ,
} ) ,
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:19:48 -05:00
status : "idle" ,
} ) ) ;
expect ( companySvc . create ) . toHaveBeenCalledWith ( expect . objectContaining ( {
[codex] Harden recovery issue handling (#4600)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane must recover stranded agent work without creating
new operational loops
> - Stranded recovery issues can themselves fail, and exposing raw retry
errors in comments can leak sensitive adapter details
> - New local companies also should not force a hire-approval gate
unless operators enable that policy
> - This pull request hardens recovery issue handling, redacts retry
failure details in issue copy, preserves `maxConcurrentRuns: 1`, and
flips new-hire approval to an opt-in default
> - The benefit is safer automatic recovery and smoother default company
setup without hidden migration conflicts
## What Changed
- Added migration `0071_default_hire_approval_off` and updated company
schema/import/export/docs so hire approvals default off and serialize
only when enabled.
- Added migration `0072_large_sandman` with a partial unique index
preventing duplicate active stranded recovery issues for the same source
issue.
- Blocked failed `stranded_issue_recovery` issues in place instead of
creating nested recovery issues.
- Redacted latest retry failure details from recovery issue comments
while still linking reviewers to run evidence.
- Allowed `maxConcurrentRuns: 1` to be honored by heartbeat concurrency
normalization.
- Added focused regression coverage for recovery recursion, redaction,
migration ordering, and concurrency behavior.
## Verification
- `pnpm --filter @paperclipai/db run check:migrations`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/recovery-classifiers.test.ts`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/company-portability.test.ts --pool=forks
--poolOptions.forks.isolate=true`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/agent-permissions-routes.test.ts --pool=forks
--poolOptions.forks.isolate=true`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-process-recovery.test.ts --pool=forks
--poolOptions.forks.isolate=true` exits 0, but this host skipped the
embedded Postgres tests with the existing init guard.
- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/heartbeat-dependency-scheduling.test.ts
--pool=forks --poolOptions.forks.isolate=true` exits 0, but this host
skipped the embedded Postgres tests with the existing init guard.
## Risks
- Migration risk is low but this PR intentionally owns both new
migrations to avoid separate PR migration-journal conflicts.
- Recovery comments now require operators to inspect linked run evidence
for details instead of reading raw errors inline.
- The hire approval default changes behavior for newly created/imported
companies only; existing persisted company settings are not changed
except by the SQL default for future rows.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5 coding agent, tool-enabled terminal/GitHub
workflow, reasoning mode active. Context window not exposed in this
environment.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-27 15:02:47 -05:00
requireBoardApprovalForNewAgents : false ,
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
} ) ) ;
} ) ;
it ( "normalizes adapter config on replace imports before updating existing agents" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
secretSvc . normalizeAdapterConfigForPersistence . mockResolvedValueOnce ( {
normalized : "updated" ,
} ) ;
agentSvc . update . mockImplementation ( async ( id : string , patch : Record < string , unknown > ) = > ( {
id ,
name : "ClaudeCoder" ,
adapterType : patch.adapterType ,
adapterConfig : patch.adapterConfig ,
} ) ) ;
await portability . importBundle ( {
source : {
type : "inline" ,
rootPath : exported.rootPath ,
files : exported.files ,
} ,
include : {
company : false ,
agents : true ,
projects : false ,
issues : false ,
} ,
target : {
mode : "existing_company" ,
companyId : "company-1" ,
} ,
agents : [ "claudecoder" ] ,
collisionStrategy : "replace" ,
adapterOverrides : {
claudecoder : {
adapterType : "codex_local" ,
adapterConfig : {
model : "gpt-5.4" ,
} ,
} ,
} ,
} , "user-1" ) ;
expect ( secretSvc . normalizeAdapterConfigForPersistence ) . toHaveBeenCalledWith (
"company-1" ,
2026-05-06 07:49:47 -05:00
expect . objectContaining ( {
model : "gpt-5.4" ,
extraArgs : [ "--skip-git-repo-check" ] ,
} ) ,
[codex] harden authenticated routes and issue editor reliability (#3741)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The control plane depends on authenticated routes enforcing company
boundaries and role permissions correctly
> - This branch also touches the issue detail and markdown editing flows
operators use while handling advisory and triage work
> - Partial issue cache seeds and fragile rich-editor parsing could
leave important issue content missing or blank at the moment an operator
needed it
> - Blocked issues becoming actionable again should wake their assignee
automatically instead of silently staying idle
> - This pull request rebases the advisory follow-up branch onto current
`master`, hardens authenticated route authorization, and carries the
issue-detail/editor reliability fixes forward with regression tests
> - The benefit is tighter authz on sensitive routes plus more reliable
issue/advisory editing and wakeup behavior on top of the latest base
## What Changed
- Hardened authenticated route authorization across agent, activity,
approval, access, project, plugin, health, execution-workspace,
portability, and related server paths, with new cross-tenant and
runtime-authz regression coverage.
- Switched issue detail queries from `initialData` to placeholder-based
hydration so list/quicklook seeds still refetch full issue bodies.
- Normalized advisory-style HTML images before mounting the markdown
editor and strengthened fallback behavior when the rich editor silently
fails or rejects the content.
- Woke assigned agents when blocked issues move back to `todo`, with
route coverage for reopen and unblock transitions.
- Rebasing note: this branch now sits cleanly on top of the latest
`master` tip used for the PR base.
## Verification
- `pnpm exec vitest run ui/src/lib/issueDetailQuery.test.tsx
ui/src/components/MarkdownEditor.test.tsx
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts`
- Confirmed `pnpm-lock.yaml` is not part of the PR diff.
- Rebased the branch onto current `public-gh/master` before publishing.
## Risks
- Broad authz tightening may expose existing flows that were relying on
permissive board or agent access and now need explicit grants.
- Markdown editor fallback changes could affect focus or rendering in
edge-case content that mixes HTML-like advisory markup with normal
markdown.
- This verification was intentionally scoped to touched regressions and
did not run the full repository suite.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment
with tool use for terminal, git, and GitHub operations. The exact
runtime model identifier is not exposed inside this session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, it is behavior-only and does not
need before/after screenshots
- [x] I have updated relevant documentation to reflect my changes, or no
documentation changes were needed for these internal fixes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-15 08:41:15 -05:00
{ strictMode : false } ,
) ;
expect ( agentSvc . update ) . toHaveBeenCalledWith ( "agent-1" , expect . objectContaining ( {
adapterType : "codex_local" ,
adapterConfig : {
normalized : "updated" ,
} ,
} ) ) ;
} ) ;
[codex] Improve agent runtime recovery and governance (#4086)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The heartbeat runtime, agent import path, and agent configuration
defaults determine whether work is dispatched safely and predictably.
> - Several accumulated fixes all touched agent execution recovery, wake
routing, import behavior, and runtime concurrency defaults.
> - Those changes need to land together so the heartbeat service and
agent creation defaults stay internally consistent.
> - This pull request groups the runtime/governance changes from the
split branch into one standalone branch.
> - The benefit is safer recovery for stranded runs, bounded high-volume
reads, imported-agent approval correctness, skill-template support, and
a clearer default concurrency policy.
## What Changed
- Fixed stranded continuation recovery so successful automatic retries
are requeued instead of incorrectly blocking the issue.
- Bounded high-volume issue/log reads across issue, heartbeat, agent,
project, and workspace paths.
- Fixed imported-agent approval and instruction-path permission
handling.
- Quarantined seeded worktree execution state during worktree
provisioning.
- Queued approval follow-up wakes and hardened SQL_ASCII heartbeat
output handling.
- Added reusable agent instruction templates for hiring flows.
- Set the default max concurrent agent runs to five and updated related
UI/tests/docs.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-list.test.ts
server/src/__tests__/issues-service.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
packages/adapter-utils/src/server-utils.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Split integration check: merged this branch first, followed by the
other [PAP-1614](/PAP/issues/PAP-1614) branches, with no merge
conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Medium risk: touches heartbeat recovery, queueing, and issue list
bounds in central runtime paths.
- Imported-agent and concurrency default behavior changes may affect
existing automation that assumes one-at-a-time default runs.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:19:48 -05:00
it ( "nameOverrides applied after collision detection do not re-validate uniqueness" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : { company : false , agents : true , projects : false , issues : false } ,
} ) ;
// Simulate existing agents so collision detection triggers rename
agentSvc . list . mockResolvedValue ( [
{ id : "existing-1" , name : "ClaudeCoder" , status : "idle" , role : "engineer" , adapterType : "claude_local" , adapterConfig : { } , runtimeConfig : { } , budgetMonthlyCents : 0 , permissions : { } , metadata : null } ,
] ) ;
const preview = await portability . previewImport ( {
source : { type : "inline" , rootPath : exported.rootPath , files : exported.files } ,
include : { company : false , agents : true , projects : false , issues : false } ,
target : { mode : "existing_company" , companyId : "company-1" } ,
agents : [ "claudecoder" ] ,
collisionStrategy : "rename" ,
nameOverrides : { claudecoder : "ClaudeCoder" } ,
} ) ;
// The override reverts the renamed agent back to its original collision name.
// This is a known limitation: nameOverrides bypass collision checks.
const plan = preview . plan . agentPlans . find ( ( p ) = > p . slug === "claudecoder" ) ;
expect ( plan ) . toBeDefined ( ) ;
expect ( plan ! . action ) . toBe ( "create" ) ;
expect ( plan ! . plannedName ) . toBe ( "ClaudeCoder" ) ;
} ) ;
it ( "handles circular reportsTo chains without infinite recursion during export" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
agentSvc . list . mockResolvedValue ( [
{
id : "agent-a" , name : "AgentA" , status : "idle" , role : "engineer" , title : null , icon : null ,
reportsTo : "agent-b" , capabilities : null , adapterType : "claude_local" ,
adapterConfig : { } , runtimeConfig : { } , budgetMonthlyCents : 0 , permissions : { } , metadata : null ,
} ,
{
id : "agent-b" , name : "AgentB" , status : "idle" , role : "manager" , title : null , icon : null ,
reportsTo : "agent-a" , capabilities : null , adapterType : "claude_local" ,
adapterConfig : { } , runtimeConfig : { } , budgetMonthlyCents : 0 , permissions : { } , metadata : null ,
} ,
] ) ;
agentInstructionsSvc . exportFiles . mockResolvedValue ( {
files : { "AGENTS.md" : "Instructions" } , entryFile : "AGENTS.md" , warnings : [ ] ,
} ) ;
// Export should complete without infinite recursion in org chart building
const exported = await portability . exportBundle ( "company-1" , {
include : { company : true , agents : true , projects : false , issues : false } ,
} ) ;
expect ( exported . manifest . agents ) . toHaveLength ( 2 ) ;
// Both agents should appear in the export
const slugs = exported . manifest . agents . map ( ( a ) = > a . slug ) ;
expect ( slugs ) . toContain ( "agenta" ) ;
expect ( slugs ) . toContain ( "agentb" ) ;
} ) ;
it ( "resolves issue assignee to existing agent when agent is skipped" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
projectSvc . list . mockResolvedValue ( [ {
id : "project-1" , companyId : "company-1" , name : "TestProject" , urlKey : "testproject" ,
description : null , leadAgentId : null , targetDate : null , color : null , status : "planned" ,
executionWorkspacePolicy : null , archivedAt : null , workspaces : [ ] ,
} ] ) ;
issueSvc . list . mockResolvedValue ( [ {
id : "issue-1" , companyId : "company-1" , title : "Test task" , identifier : "PAP-1" ,
description : "A test task" , status : "todo" , priority : "medium" ,
assigneeAgentId : "agent-1" , projectId : "project-1" , projectWorkspaceId : null ,
goalId : null , parentId : null , billingCode : null , labelIds : [ ] ,
executionWorkspaceSettings : null , assigneeAdapterOverrides : null , metadata : null ,
} ] ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : { company : false , agents : true , projects : true , issues : true } ,
} ) ;
// Re-import into same company with skip collision strategy
// Both agents exist so both will be skipped; the existing agent should resolve for issue assignment
agentSvc . list . mockResolvedValue ( [
{ id : "agent-1" , name : "ClaudeCoder" , status : "idle" , role : "engineer" , adapterType : "claude_local" , adapterConfig : { } , runtimeConfig : { } , budgetMonthlyCents : 0 , permissions : { } , metadata : null } ,
{ id : "agent-2" , name : "CMO" , status : "idle" , role : "cmo" , adapterType : "claude_local" , adapterConfig : { } , runtimeConfig : { } , budgetMonthlyCents : 0 , permissions : { } , metadata : null } ,
] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
issueSvc . list . mockResolvedValue ( [ ] ) ;
projectSvc . create . mockResolvedValue ( { id : "project-new" , companyId : "company-1" , urlKey : "testproject" } ) ;
issueSvc . create . mockResolvedValue ( { id : "issue-new" , identifier : "PAP-100" } ) ;
const result = await portability . importBundle ( {
source : { type : "inline" , rootPath : exported.rootPath , files : exported.files } ,
include : { company : false , agents : true , projects : true , issues : true } ,
target : { mode : "existing_company" , companyId : "company-1" } ,
agents : "all" ,
collisionStrategy : "skip" ,
} , "user-1" ) ;
// Both agents should be skipped (already exist)
const agentResult = result . agents . find ( ( a ) = > a . slug === "claudecoder" ) ;
expect ( agentResult ) . toBeDefined ( ) ;
expect ( agentResult ! . action ) . toBe ( "skipped" ) ;
// Issue should still be created and reference the existing agent
expect ( issueSvc . create ) . toHaveBeenCalled ( ) ;
const issueCreateCall = issueSvc . create . mock . calls [ 0 ] ;
// The assigneeAgentId should resolve to the existing agent via existingSlugToAgentId
expect ( issueCreateCall [ 1 ] ) . toEqual ( expect . objectContaining ( {
assigneeAgentId : "agent-1" ,
} ) ) ;
} ) ;
it ( "handles a package with only skills (no agents or projects)" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : { company : false , agents : false , projects : false , issues : false , skills : true } ,
expandReferencedSkills : true ,
} ) ;
expect ( exported . manifest . agents ) . toHaveLength ( 0 ) ;
expect ( exported . manifest . projects ) . toHaveLength ( 0 ) ;
expect ( exported . manifest . issues ) . toHaveLength ( 0 ) ;
// Skills should still be exported
expect ( exported . manifest . skills . length ) . toBeGreaterThanOrEqual ( 0 ) ;
} ) ;
it ( "preview import detects no agents to import when agents are excluded" , async ( ) = > {
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : { company : true , agents : true , projects : false , issues : false } ,
} ) ;
agentSvc . list . mockResolvedValue ( [ ] ) ;
const preview = await portability . previewImport ( {
source : { type : "inline" , rootPath : exported.rootPath , files : exported.files } ,
include : { company : false , agents : false , projects : false , issues : false } ,
target : { mode : "existing_company" , companyId : "company-1" } ,
agents : "all" ,
collisionStrategy : "rename" ,
} ) ;
expect ( preview . plan . agentPlans ) . toHaveLength ( 0 ) ;
expect ( preview . plan . projectPlans ) . toHaveLength ( 0 ) ;
expect ( preview . plan . issuePlans ) . toHaveLength ( 0 ) ;
} ) ;
2026-03-14 09:46:16 -05:00
} ) ;