2026-03-10 14:55:35 -05:00
import {
chmodSync ,
copyFileSync ,
existsSync ,
mkdirSync ,
2026-03-20 15:59:41 -05:00
promises as fsPromises ,
2026-03-10 14:55:35 -05:00
readdirSync ,
readFileSync ,
readlinkSync ,
rmSync ,
statSync ,
symlinkSync ,
writeFileSync ,
} from "node:fs" ;
2026-03-10 10:08:13 -05:00
import os from "node:os" ;
import path from "node:path" ;
import { execFileSync } from "node:child_process" ;
import { createServer } from "node:net" ;
2026-03-20 15:59:41 -05:00
import { Readable } from "node:stream" ;
2026-03-10 10:08:13 -05:00
import * as p from "@clack/prompts" ;
import pc from "picocolors" ;
2026-03-20 15:02:24 -05:00
import { and , eq , inArray , sql } from "drizzle-orm" ;
2026-03-10 10:08:13 -05:00
import {
2026-03-10 10:08:58 -05:00
applyPendingMigrations ,
2026-03-20 15:02:24 -05:00
agents ,
2026-03-20 15:59:41 -05:00
assets ,
2026-03-20 15:02:24 -05:00
companies ,
2026-03-10 13:50:29 -05:00
createDb ,
2026-03-20 15:59:41 -05:00
documentRevisions ,
documents ,
2026-03-10 10:08:13 -05:00
ensurePostgresDatabase ,
formatDatabaseBackupResult ,
2026-03-20 15:02:24 -05:00
goals ,
heartbeatRuns ,
2026-03-20 17:23:45 -05:00
inspectMigrations ,
2026-03-20 15:59:41 -05:00
issueAttachments ,
2026-03-20 15:02:24 -05:00
issueComments ,
issueDocuments ,
issues ,
2026-03-10 13:50:29 -05:00
projectWorkspaces ,
2026-03-20 15:02:24 -05:00
projects ,
2026-04-10 22:26:30 -05:00
routines ,
routineTriggers ,
2026-03-10 10:08:13 -05:00
runDatabaseBackup ,
runDatabaseRestore ,
2026-03-26 11:34:16 -05:00
createEmbeddedPostgresLogBuffer ,
formatEmbeddedPostgresError ,
2026-03-10 10:08:13 -05:00
} from "@paperclipai/db" ;
import type { Command } from "commander" ;
import { ensureAgentJwtSecret , loadPaperclipEnvFile , mergePaperclipEnvEntries , readPaperclipEnvEntries , resolvePaperclipEnvFile } from "../config/env.js" ;
import { expandHomePrefix } from "../config/home.js" ;
import type { PaperclipConfig } from "../config/schema.js" ;
import { readConfig , resolveConfigPath , writeConfig } from "../config/store.js" ;
import { printPaperclipCliBanner } from "../utils/banner.js" ;
2026-03-10 12:57:53 -05:00
import { resolveRuntimeLikePath } from "../utils/path-resolver.js" ;
2026-03-10 10:08:13 -05:00
import {
buildWorktreeConfig ,
buildWorktreeEnvEntries ,
DEFAULT_WORKTREE_HOME ,
formatShellExports ,
2026-03-13 11:12:43 -05:00
generateWorktreeColor ,
2026-03-10 07:41:01 -05:00
isWorktreeSeedMode ,
2026-03-10 10:08:13 -05:00
resolveSuggestedWorktreeName ,
2026-03-10 07:41:01 -05:00
resolveWorktreeSeedPlan ,
2026-03-10 10:08:13 -05:00
resolveWorktreeLocalPaths ,
sanitizeWorktreeInstanceId ,
2026-03-10 07:41:01 -05:00
type WorktreeSeedMode ,
2026-03-10 10:08:13 -05:00
type WorktreeLocalPaths ,
} from "./worktree-lib.js" ;
2026-03-20 15:02:24 -05:00
import {
buildWorktreeMergePlan ,
parseWorktreeMergeScopes ,
2026-03-20 15:59:41 -05:00
type IssueAttachmentRow ,
type IssueDocumentRow ,
type DocumentRevisionRow ,
type PlannedAttachmentInsert ,
2026-03-20 15:02:24 -05:00
type PlannedCommentInsert ,
2026-03-20 15:59:41 -05:00
type PlannedIssueDocumentInsert ,
type PlannedIssueDocumentMerge ,
2026-03-20 15:02:24 -05:00
type PlannedIssueInsert ,
} from "./worktree-merge-history-lib.js" ;
2026-03-10 10:08:13 -05:00
type WorktreeInitOptions = {
name? : string ;
2026-04-07 17:02:34 -05:00
color? : string ;
2026-03-10 10:08:13 -05:00
instance? : string ;
home? : string ;
fromConfig? : string ;
fromDataDir? : string ;
fromInstance? : string ;
2026-03-13 14:24:06 -05:00
sourceConfigPathOverride? : string ;
2026-03-10 10:08:13 -05:00
serverPort? : number ;
dbPort? : number ;
seed? : boolean ;
2026-03-10 07:41:01 -05:00
seedMode? : string ;
[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
preserveLiveWork? : boolean ;
2026-03-10 10:08:13 -05:00
force? : boolean ;
} ;
2026-03-11 09:15:27 -05:00
type WorktreeMakeOptions = WorktreeInitOptions & {
startPoint? : string ;
} ;
2026-03-10 16:52:26 -05:00
2026-03-10 10:08:13 -05:00
type WorktreeEnvOptions = {
config? : string ;
json? : boolean ;
} ;
2026-03-20 15:17:51 -05:00
type WorktreeListOptions = {
json? : boolean ;
} ;
2026-03-20 15:02:24 -05:00
type WorktreeMergeHistoryOptions = {
2026-03-20 15:39:02 -05:00
from ? : string ;
to? : string ;
2026-03-20 15:02:24 -05:00
company? : string ;
scope? : string ;
apply? : boolean ;
dry? : boolean ;
yes? : boolean ;
} ;
2026-04-07 07:48:22 -05:00
type WorktreeReseedOptions = {
from ? : string ;
to? : string ;
fromConfig? : string ;
fromDataDir? : string ;
fromInstance? : string ;
seedMode? : string ;
[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
preserveLiveWork? : boolean ;
2026-04-07 07:48:22 -05:00
yes? : boolean ;
allowLiveTarget? : boolean ;
} ;
[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
type WorktreeRepairOptions = {
branch? : string ;
home? : string ;
fromConfig? : string ;
fromDataDir? : string ;
fromInstance? : string ;
seedMode? : string ;
[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
preserveLiveWork? : boolean ;
[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
noSeed? : boolean ;
allowLiveTarget? : boolean ;
} ;
2026-03-10 10:08:13 -05:00
type EmbeddedPostgresInstance = {
initialise ( ) : Promise < void > ;
start ( ) : Promise < void > ;
stop ( ) : Promise < void > ;
} ;
type EmbeddedPostgresCtor = new ( opts : {
databaseDir : string ;
user : string ;
password : string ;
port : number ;
persistent : boolean ;
2026-03-13 09:25:04 -05:00
initdbFlags? : string [ ] ;
2026-03-10 10:08:13 -05:00
onLog ? : ( message : unknown ) = > void ;
onError ? : ( message : unknown ) = > void ;
} ) = > EmbeddedPostgresInstance ;
type EmbeddedPostgresHandle = {
port : number ;
startedByThisProcess : boolean ;
stop : ( ) = > Promise < void > ;
} ;
2026-03-10 13:50:29 -05:00
type GitWorkspaceInfo = {
root : string ;
commonDir : string ;
2026-03-10 14:55:35 -05:00
gitDir : string ;
hooksPath : string ;
} ;
type CopiedGitHooksResult = {
sourceHooksPath : string ;
targetHooksPath : string ;
copied : boolean ;
2026-03-10 13:50:29 -05:00
} ;
type SeedWorktreeDatabaseResult = {
backupSummary : string ;
[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
pausedScheduledRoutines : number ;
executionQuarantine : SeededWorktreeExecutionQuarantineSummary ;
2026-03-10 13:50:29 -05:00
reboundWorkspaces : Array < {
name : string ;
fromCwd : string ;
toCwd : string ;
} > ;
} ;
[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
export type SeededWorktreeExecutionQuarantineSummary = {
disabledTimerHeartbeats : number ;
resetRunningAgents : number ;
quarantinedInProgressIssues : number ;
unassignedTodoIssues : number ;
unassignedReviewIssues : number ;
} ;
2026-03-10 10:08:13 -05:00
function nonEmpty ( value : string | null | undefined ) : string | null {
return typeof value === "string" && value . trim ( ) . length > 0 ? value . trim ( ) : null ;
}
2026-03-11 16:38:31 -05:00
function isCurrentSourceConfigPath ( sourceConfigPath : string ) : boolean {
const currentConfigPath = process . env . PAPERCLIP_CONFIG ;
if ( ! currentConfigPath || currentConfigPath . trim ( ) . length === 0 ) {
return false ;
}
return path . resolve ( currentConfigPath ) === path . resolve ( sourceConfigPath ) ;
}
[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
function formatSeededWorktreeExecutionQuarantineSummary (
summary : SeededWorktreeExecutionQuarantineSummary ,
) : string {
return [
` disabled timer heartbeats: ${ summary . disabledTimerHeartbeats } ` ,
` reset running agents: ${ summary . resetRunningAgents } ` ,
` quarantined in-progress issues: ${ summary . quarantinedInProgressIssues } ` ,
` unassigned todo issues: ${ summary . unassignedTodoIssues } ` ,
` unassigned review issues: ${ summary . unassignedReviewIssues } ` ,
] . join ( ", " ) ;
}
2026-03-13 07:24:39 -05:00
const WORKTREE_NAME_PREFIX = "paperclip-" ;
2026-03-10 16:52:26 -05:00
function resolveWorktreeMakeName ( name : string ) : string {
const value = nonEmpty ( name ) ;
if ( ! value ) {
throw new Error ( "Worktree name is required." ) ;
}
if ( ! /^[A-Za-z0-9._-]+$/ . test ( value ) ) {
throw new Error (
"Worktree name must contain only letters, numbers, dots, underscores, or dashes." ,
) ;
}
2026-03-13 07:24:39 -05:00
return value . startsWith ( WORKTREE_NAME_PREFIX ) ? value : ` ${ WORKTREE_NAME_PREFIX } ${ value } ` ;
}
function resolveWorktreeHome ( explicit? : string ) : string {
return explicit ? ? process . env . PAPERCLIP_WORKTREES_DIR ? ? DEFAULT_WORKTREE_HOME ;
}
function resolveWorktreeStartPoint ( explicit? : string ) : string | undefined {
return explicit ? ? nonEmpty ( process . env . PAPERCLIP_WORKTREE_START_POINT ) ? ? undefined ;
2026-03-10 16:52:26 -05:00
}
2026-03-20 15:59:41 -05:00
type ConfiguredStorage = {
getObject ( companyId : string , objectKey : string ) : Promise < Buffer > ;
putObject ( companyId : string , objectKey : string , body : Buffer , contentType : string ) : Promise < void > ;
} ;
function assertStorageCompanyPrefix ( companyId : string , objectKey : string ) : void {
if ( ! objectKey . startsWith ( ` ${ companyId } / ` ) || objectKey . includes ( ".." ) ) {
throw new Error ( ` Invalid object key for company ${ companyId } . ` ) ;
}
}
function normalizeStorageObjectKey ( objectKey : string ) : string {
const normalized = objectKey . replace ( /\\/g , "/" ) . trim ( ) ;
if ( ! normalized || normalized . startsWith ( "/" ) ) {
throw new Error ( "Invalid object key." ) ;
}
const parts = normalized . split ( "/" ) . filter ( ( part ) = > part . length > 0 ) ;
if ( parts . length === 0 || parts . some ( ( part ) = > part === "." || part === ".." ) ) {
throw new Error ( "Invalid object key." ) ;
}
return parts . join ( "/" ) ;
}
function resolveLocalStoragePath ( baseDir : string , objectKey : string ) : string {
const resolved = path . resolve ( baseDir , normalizeStorageObjectKey ( objectKey ) ) ;
const root = path . resolve ( baseDir ) ;
if ( resolved !== root && ! resolved . startsWith ( ` ${ root } ${ path . sep } ` ) ) {
throw new Error ( "Invalid object key path." ) ;
}
return resolved ;
}
async function s3BodyToBuffer ( body : unknown ) : Promise < Buffer > {
if ( ! body ) {
throw new Error ( "Object not found." ) ;
}
if ( Buffer . isBuffer ( body ) ) {
return body ;
}
if ( body instanceof Readable ) {
return await streamToBuffer ( body ) ;
}
const candidate = body as {
transformToWebStream ? : ( ) = > ReadableStream < Uint8Array > ;
arrayBuffer ? : ( ) = > Promise < ArrayBuffer > ;
} ;
if ( typeof candidate . transformToWebStream === "function" ) {
const webStream = candidate . transformToWebStream ( ) ;
const reader = webStream . getReader ( ) ;
const chunks : Uint8Array [ ] = [ ] ;
while ( true ) {
const { done , value } = await reader . read ( ) ;
if ( done ) break ;
if ( value ) chunks . push ( value ) ;
}
return Buffer . concat ( chunks . map ( ( chunk ) = > Buffer . from ( chunk ) ) ) ;
}
if ( typeof candidate . arrayBuffer === "function" ) {
return Buffer . from ( await candidate . arrayBuffer ( ) ) ;
}
throw new Error ( "Unsupported storage response body." ) ;
}
function normalizeS3Prefix ( prefix : string | undefined ) : string {
if ( ! prefix ) return "" ;
return prefix . trim ( ) . replace ( /^\/+/ , "" ) . replace ( /\/+$/ , "" ) ;
}
function buildS3ObjectKey ( prefix : string , objectKey : string ) : string {
return prefix ? ` ${ prefix } / ${ objectKey } ` : objectKey ;
}
const dynamicImport = new Function ( "specifier" , "return import(specifier);" ) as ( specifier : string ) = > Promise < any > ;
function createConfiguredStorageFromPaperclipConfig ( config : PaperclipConfig ) : ConfiguredStorage {
if ( config . storage . provider === "local_disk" ) {
const baseDir = expandHomePrefix ( config . storage . localDisk . baseDir ) ;
return {
async getObject ( companyId : string , objectKey : string ) {
assertStorageCompanyPrefix ( companyId , objectKey ) ;
return await fsPromises . readFile ( resolveLocalStoragePath ( baseDir , objectKey ) ) ;
} ,
async putObject ( companyId : string , objectKey : string , body : Buffer ) {
assertStorageCompanyPrefix ( companyId , objectKey ) ;
const filePath = resolveLocalStoragePath ( baseDir , objectKey ) ;
await fsPromises . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
await fsPromises . writeFile ( filePath , body ) ;
} ,
} ;
}
const prefix = normalizeS3Prefix ( config . storage . s3 . prefix ) ;
let s3ClientPromise : Promise < any > | null = null ;
async function getS3Client() {
if ( ! s3ClientPromise ) {
s3ClientPromise = ( async ( ) = > {
const sdk = await dynamicImport ( "@aws-sdk/client-s3" ) ;
return {
sdk ,
client : new sdk . S3Client ( {
region : config.storage.s3.region ,
endpoint : config.storage.s3.endpoint ,
forcePathStyle : config.storage.s3.forcePathStyle ,
} ) ,
} ;
} ) ( ) ;
}
return await s3ClientPromise ;
}
const bucket = config . storage . s3 . bucket ;
return {
async getObject ( companyId : string , objectKey : string ) {
assertStorageCompanyPrefix ( companyId , objectKey ) ;
const { sdk , client } = await getS3Client ( ) ;
const response = await client . send (
new sdk . GetObjectCommand ( {
Bucket : bucket ,
Key : buildS3ObjectKey ( prefix , objectKey ) ,
} ) ,
) ;
return await s3BodyToBuffer ( response . Body ) ;
} ,
async putObject ( companyId : string , objectKey : string , body : Buffer , contentType : string ) {
assertStorageCompanyPrefix ( companyId , objectKey ) ;
const { sdk , client } = await getS3Client ( ) ;
await client . send (
new sdk . PutObjectCommand ( {
Bucket : bucket ,
Key : buildS3ObjectKey ( prefix , objectKey ) ,
Body : body ,
ContentType : contentType ,
ContentLength : body.length ,
} ) ,
) ;
} ,
} ;
}
function openConfiguredStorage ( configPath : string ) : ConfiguredStorage {
const config = readConfig ( configPath ) ;
if ( ! config ) {
throw new Error ( ` Config not found at ${ configPath } . ` ) ;
}
return createConfiguredStorageFromPaperclipConfig ( config ) ;
}
async function streamToBuffer ( stream : NodeJS.ReadableStream ) : Promise < Buffer > {
const chunks : Buffer [ ] = [ ] ;
for await ( const chunk of stream ) {
chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer.from ( chunk ) ) ;
}
return Buffer . concat ( chunks ) ;
}
2026-03-20 16:06:41 -05:00
export function isMissingStorageObjectError ( error : unknown ) : boolean {
if ( ! error || typeof error !== "object" ) return false ;
const candidate = error as { code? : unknown ; status? : unknown ; name? : unknown ; message? : unknown } ;
return candidate . code === "ENOENT"
|| candidate . status === 404
|| candidate . name === "NoSuchKey"
|| candidate . name === "NotFound"
|| candidate . message === "Object not found." ;
}
export async function readSourceAttachmentBody (
2026-03-20 16:12:10 -05:00
sourceStorages : Array < Pick < ConfiguredStorage , "getObject" > > ,
2026-03-20 16:06:41 -05:00
companyId : string ,
objectKey : string ,
) : Promise < Buffer | null > {
2026-03-20 16:12:10 -05:00
for ( const sourceStorage of sourceStorages ) {
try {
return await sourceStorage . getObject ( companyId , objectKey ) ;
} catch ( error ) {
if ( isMissingStorageObjectError ( error ) ) {
continue ;
}
throw error ;
2026-03-20 16:06:41 -05:00
}
}
2026-03-20 16:12:10 -05:00
return null ;
2026-03-20 16:06:41 -05:00
}
2026-03-10 16:52:26 -05:00
export function resolveWorktreeMakeTargetPath ( name : string ) : string {
return path . resolve ( os . homedir ( ) , resolveWorktreeMakeName ( name ) ) ;
}
function extractExecSyncErrorMessage ( error : unknown ) : string | null {
if ( ! error || typeof error !== "object" ) {
return error instanceof Error ? error.message : null ;
}
const stderr = "stderr" in error ? error.stderr : null ;
if ( typeof stderr === "string" ) {
return nonEmpty ( stderr ) ;
}
if ( stderr instanceof Buffer ) {
return nonEmpty ( stderr . toString ( "utf8" ) ) ;
}
return error instanceof Error ? nonEmpty ( error . message ) : null ;
}
function localBranchExists ( cwd : string , branchName : string ) : boolean {
try {
execFileSync ( "git" , [ "show-ref" , "--verify" , "--quiet" , ` refs/heads/ ${ branchName } ` ] , {
cwd ,
stdio : "ignore" ,
} ) ;
return true ;
} catch {
return false ;
}
}
export function resolveGitWorktreeAddArgs ( input : {
branchName : string ;
targetPath : string ;
branchExists : boolean ;
2026-03-11 09:15:27 -05:00
startPoint? : string ;
2026-03-10 16:52:26 -05:00
} ) : string [ ] {
2026-03-11 09:15:27 -05:00
if ( input . branchExists && ! input . startPoint ) {
2026-03-10 16:52:26 -05:00
return [ "worktree" , "add" , input . targetPath , input . branchName ] ;
}
2026-03-11 09:15:27 -05:00
const commitish = input . startPoint ? ? "HEAD" ;
return [ "worktree" , "add" , "-b" , input . branchName , input . targetPath , commitish ] ;
2026-03-10 16:52:26 -05:00
}
2026-03-10 10:08:13 -05:00
function readPidFilePort ( postmasterPidFile : string ) : number | null {
if ( ! existsSync ( postmasterPidFile ) ) return null ;
try {
const lines = readFileSync ( postmasterPidFile , "utf8" ) . split ( "\n" ) ;
const port = Number ( lines [ 3 ] ? . trim ( ) ) ;
return Number . isInteger ( port ) && port > 0 ? port : null ;
} catch {
return null ;
}
}
function readRunningPostmasterPid ( postmasterPidFile : string ) : number | null {
if ( ! existsSync ( postmasterPidFile ) ) return null ;
try {
const pid = Number ( readFileSync ( postmasterPidFile , "utf8" ) . split ( "\n" ) [ 0 ] ? . trim ( ) ) ;
if ( ! Number . isInteger ( pid ) || pid <= 0 ) return null ;
process . kill ( pid , 0 ) ;
return pid ;
} catch {
return null ;
}
}
async function isPortAvailable ( port : number ) : Promise < boolean > {
return await new Promise < boolean > ( ( resolve ) = > {
const server = createServer ( ) ;
server . unref ( ) ;
server . once ( "error" , ( ) = > resolve ( false ) ) ;
server . listen ( port , "127.0.0.1" , ( ) = > {
server . close ( ( ) = > resolve ( true ) ) ;
} ) ;
} ) ;
}
async function findAvailablePort ( preferredPort : number , reserved = new Set < number > ( ) ) : Promise < number > {
let port = Math . max ( 1 , Math . trunc ( preferredPort ) ) ;
while ( reserved . has ( port ) || ! ( await isPortAvailable ( port ) ) ) {
port += 1 ;
}
return port ;
}
2026-03-26 10:56:13 -05:00
function resolveRepoManagedWorktreesRoot ( cwd : string ) : string | null {
const normalized = path . resolve ( cwd ) ;
const marker = ` ${ path . sep } .paperclip ${ path . sep } worktrees ${ path . sep } ` ;
const index = normalized . indexOf ( marker ) ;
if ( index === - 1 ) return null ;
const repoRoot = normalized . slice ( 0 , index ) ;
return path . resolve ( repoRoot , ".paperclip" , "worktrees" ) ;
}
function collectClaimedWorktreePorts ( homeDir : string , currentInstanceId : string , cwd : string ) : {
serverPorts : Set < number > ;
databasePorts : Set < number > ;
} {
const serverPorts = new Set < number > ( ) ;
const databasePorts = new Set < number > ( ) ;
const configPaths = new Set < string > ( ) ;
const instancesDir = path . resolve ( homeDir , "instances" ) ;
if ( existsSync ( instancesDir ) ) {
for ( const entry of readdirSync ( instancesDir , { withFileTypes : true } ) ) {
if ( ! entry . isDirectory ( ) || entry . name === currentInstanceId ) continue ;
const configPath = path . resolve ( instancesDir , entry . name , "config.json" ) ;
if ( existsSync ( configPath ) ) {
configPaths . add ( configPath ) ;
}
}
}
const repoManagedWorktreesRoot = resolveRepoManagedWorktreesRoot ( cwd ) ;
if ( repoManagedWorktreesRoot && existsSync ( repoManagedWorktreesRoot ) ) {
for ( const entry of readdirSync ( repoManagedWorktreesRoot , { withFileTypes : true } ) ) {
if ( ! entry . isDirectory ( ) ) continue ;
const configPath = path . resolve ( repoManagedWorktreesRoot , entry . name , ".paperclip" , "config.json" ) ;
if ( existsSync ( configPath ) ) {
configPaths . add ( configPath ) ;
}
}
}
for ( const configPath of configPaths ) {
try {
const config = readConfig ( configPath ) ;
if ( config ? . server . port ) {
serverPorts . add ( config . server . port ) ;
}
if ( config ? . database . mode === "embedded-postgres" ) {
databasePorts . add ( config . database . embeddedPostgresPort ) ;
}
} catch {
// Ignore malformed sibling configs.
}
}
return { serverPorts , databasePorts } ;
}
2026-03-10 10:08:13 -05:00
function detectGitBranchName ( cwd : string ) : string | null {
try {
const value = execFileSync ( "git" , [ "branch" , "--show-current" ] , {
cwd ,
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "ignore" ] ,
} ) . trim ( ) ;
return nonEmpty ( value ) ;
} catch {
return null ;
}
}
[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
function validateGitBranchName ( cwd : string , branchName : string ) : string {
const value = nonEmpty ( branchName ) ;
if ( ! value ) {
throw new Error ( "Branch name is required." ) ;
}
try {
execFileSync ( "git" , [ "check-ref-format" , "--branch" , value ] , {
cwd ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
} catch ( error ) {
throw new Error ( ` Invalid branch name " ${ branchName } ": ${ extractExecSyncErrorMessage ( error ) ? ? String ( error ) } ` ) ;
}
return value ;
}
function isPrimaryGitWorktree ( cwd : string ) : boolean {
const workspace = detectGitWorkspaceInfo ( cwd ) ;
return Boolean ( workspace && workspace . gitDir === workspace . commonDir ) ;
}
function resolvePrimaryGitRepoRoot ( cwd : string ) : string {
const workspace = detectGitWorkspaceInfo ( cwd ) ;
if ( ! workspace ) {
throw new Error ( "Current directory is not inside a git repository." ) ;
}
if ( workspace . gitDir === workspace . commonDir ) {
return workspace . root ;
}
return path . resolve ( workspace . commonDir , ".." ) ;
}
function resolveRepairWorktreeDirName ( branchName : string ) : string {
const normalized = branchName . trim ( )
. replace ( /[^A-Za-z0-9._-]+/g , "-" )
. replace ( /-+/g , "-" )
. replace ( /^[-._]+|[-._]+$/g , "" ) ;
return normalized || "worktree" ;
}
2026-03-10 13:50:29 -05:00
function detectGitWorkspaceInfo ( cwd : string ) : GitWorkspaceInfo | null {
try {
const root = execFileSync ( "git" , [ "rev-parse" , "--show-toplevel" ] , {
cwd ,
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "ignore" ] ,
} ) . trim ( ) ;
const commonDirRaw = execFileSync ( "git" , [ "rev-parse" , "--git-common-dir" ] , {
cwd : root ,
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "ignore" ] ,
} ) . trim ( ) ;
2026-03-10 14:55:35 -05:00
const gitDirRaw = execFileSync ( "git" , [ "rev-parse" , "--git-dir" ] , {
cwd : root ,
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "ignore" ] ,
} ) . trim ( ) ;
const hooksPathRaw = execFileSync ( "git" , [ "rev-parse" , "--git-path" , "hooks" ] , {
cwd : root ,
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "ignore" ] ,
} ) . trim ( ) ;
2026-03-10 13:50:29 -05:00
return {
root : path.resolve ( root ) ,
commonDir : path.resolve ( root , commonDirRaw ) ,
2026-03-10 14:55:35 -05:00
gitDir : path.resolve ( root , gitDirRaw ) ,
hooksPath : path.resolve ( root , hooksPathRaw ) ,
2026-03-10 13:50:29 -05:00
} ;
} catch {
return null ;
}
}
2026-03-10 14:55:35 -05:00
function copyDirectoryContents ( sourceDir : string , targetDir : string ) : boolean {
if ( ! existsSync ( sourceDir ) ) return false ;
const entries = readdirSync ( sourceDir , { withFileTypes : true } ) ;
if ( entries . length === 0 ) return false ;
mkdirSync ( targetDir , { recursive : true } ) ;
let copied = false ;
for ( const entry of entries ) {
const sourcePath = path . resolve ( sourceDir , entry . name ) ;
const targetPath = path . resolve ( targetDir , entry . name ) ;
if ( entry . isDirectory ( ) ) {
mkdirSync ( targetPath , { recursive : true } ) ;
copyDirectoryContents ( sourcePath , targetPath ) ;
copied = true ;
continue ;
}
if ( entry . isSymbolicLink ( ) ) {
rmSync ( targetPath , { recursive : true , force : true } ) ;
symlinkSync ( readlinkSync ( sourcePath ) , targetPath ) ;
copied = true ;
continue ;
}
copyFileSync ( sourcePath , targetPath ) ;
try {
chmodSync ( targetPath , statSync ( sourcePath ) . mode & 0 o777 ) ;
} catch {
// best effort
}
copied = true ;
}
return copied ;
}
export function copyGitHooksToWorktreeGitDir ( cwd : string ) : CopiedGitHooksResult | null {
const workspace = detectGitWorkspaceInfo ( cwd ) ;
if ( ! workspace ) return null ;
const sourceHooksPath = workspace . hooksPath ;
const targetHooksPath = path . resolve ( workspace . gitDir , "hooks" ) ;
if ( sourceHooksPath === targetHooksPath ) {
return {
sourceHooksPath ,
targetHooksPath ,
copied : false ,
} ;
}
return {
sourceHooksPath ,
targetHooksPath ,
copied : copyDirectoryContents ( sourceHooksPath , targetHooksPath ) ,
} ;
}
2026-03-10 13:50:29 -05:00
export function rebindWorkspaceCwd ( input : {
sourceRepoRoot : string ;
targetRepoRoot : string ;
workspaceCwd : string ;
} ) : string | null {
const sourceRepoRoot = path . resolve ( input . sourceRepoRoot ) ;
const targetRepoRoot = path . resolve ( input . targetRepoRoot ) ;
const workspaceCwd = path . resolve ( input . workspaceCwd ) ;
const relative = path . relative ( sourceRepoRoot , workspaceCwd ) ;
if ( ! relative || relative === "" ) {
return targetRepoRoot ;
}
if ( relative . startsWith ( ".." ) || path . isAbsolute ( relative ) ) {
return null ;
}
return path . resolve ( targetRepoRoot , relative ) ;
}
async function rebindSeededProjectWorkspaces ( input : {
targetConnectionString : string ;
currentCwd : string ;
} ) : Promise < SeedWorktreeDatabaseResult [ "reboundWorkspaces" ] > {
const targetRepo = detectGitWorkspaceInfo ( input . currentCwd ) ;
if ( ! targetRepo ) return [ ] ;
const db = createDb ( input . targetConnectionString ) ;
const closableDb = db as typeof db & {
$client ? : { end ? : ( opts ? : { timeout? : number } ) = > Promise < void > } ;
} ;
try {
const rows = await db
. select ( {
id : projectWorkspaces.id ,
name : projectWorkspaces.name ,
cwd : projectWorkspaces.cwd ,
} )
. from ( projectWorkspaces ) ;
const rebound : SeedWorktreeDatabaseResult [ "reboundWorkspaces" ] = [ ] ;
for ( const row of rows ) {
const workspaceCwd = nonEmpty ( row . cwd ) ;
if ( ! workspaceCwd ) continue ;
const sourceRepo = detectGitWorkspaceInfo ( workspaceCwd ) ;
if ( ! sourceRepo ) continue ;
if ( sourceRepo . commonDir !== targetRepo . commonDir ) continue ;
const reboundCwd = rebindWorkspaceCwd ( {
sourceRepoRoot : sourceRepo.root ,
targetRepoRoot : targetRepo.root ,
workspaceCwd ,
} ) ;
if ( ! reboundCwd ) continue ;
const normalizedCurrent = path . resolve ( workspaceCwd ) ;
if ( reboundCwd === normalizedCurrent ) continue ;
if ( ! existsSync ( reboundCwd ) ) continue ;
await db
. update ( projectWorkspaces )
. set ( {
cwd : reboundCwd ,
updatedAt : new Date ( ) ,
} )
. where ( eq ( projectWorkspaces . id , row . id ) ) ;
rebound . push ( {
name : row.name ,
fromCwd : normalizedCurrent ,
toCwd : reboundCwd ,
} ) ;
}
return rebound ;
} finally {
await closableDb . $client ? . end ? . ( { timeout : 5 } ) . catch ( ( ) = > undefined ) ;
}
}
2026-03-13 14:24:06 -05:00
export function resolveSourceConfigPath ( opts : WorktreeInitOptions ) : string {
if ( opts . sourceConfigPathOverride ) return path . resolve ( opts . sourceConfigPathOverride ) ;
2026-03-10 10:08:13 -05:00
if ( opts . fromConfig ) return path . resolve ( opts . fromConfig ) ;
2026-03-13 14:24:06 -05:00
if ( ! opts . fromDataDir && ! opts . fromInstance ) {
return resolveConfigPath ( ) ;
}
2026-03-10 10:08:13 -05:00
const sourceHome = path . resolve ( expandHomePrefix ( opts . fromDataDir ? ? "~/.paperclip" ) ) ;
const sourceInstanceId = sanitizeWorktreeInstanceId ( opts . fromInstance ? ? "default" ) ;
return path . resolve ( sourceHome , "instances" , sourceInstanceId , "config.json" ) ;
}
2026-04-07 07:48:22 -05:00
export function resolveWorktreeReseedSource ( input : WorktreeReseedOptions ) : ResolvedWorktreeReseedSource {
const fromSelector = nonEmpty ( input . from ) ;
const fromConfig = nonEmpty ( input . fromConfig ) ;
const fromDataDir = nonEmpty ( input . fromDataDir ) ;
const fromInstance = nonEmpty ( input . fromInstance ) ;
const hasExplicitConfigSource = Boolean ( fromConfig || fromDataDir || fromInstance ) ;
if ( fromSelector && hasExplicitConfigSource ) {
throw new Error (
"Use either --from <worktree> or --from-config/--from-data-dir/--from-instance, not both." ,
) ;
}
if ( fromSelector ) {
const endpoint = resolveWorktreeEndpointFromSelector ( fromSelector , { allowCurrent : true } ) ;
return {
configPath : endpoint.configPath ,
label : endpoint.label ,
} ;
}
if ( hasExplicitConfigSource ) {
const configPath = resolveSourceConfigPath ( {
fromConfig : fromConfig ? ? undefined ,
fromDataDir : fromDataDir ? ? undefined ,
fromInstance : fromInstance ? ? undefined ,
} ) ;
return {
configPath ,
label : configPath ,
} ;
}
throw new Error (
"Pass --from <worktree> or --from-config/--from-instance explicitly so the reseed source is unambiguous." ,
) ;
}
[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
function resolveWorktreeRepairSource ( input : WorktreeRepairOptions ) : ResolvedWorktreeReseedSource {
const fromConfig = nonEmpty ( input . fromConfig ) ;
const fromDataDir = nonEmpty ( input . fromDataDir ) ;
const fromInstance = nonEmpty ( input . fromInstance ) ? ? "default" ;
const configPath = resolveSourceConfigPath ( {
fromConfig : fromConfig ? ? undefined ,
fromDataDir : fromDataDir ? ? undefined ,
fromInstance ,
} ) ;
return {
configPath ,
label : configPath ,
} ;
}
2026-04-07 07:48:22 -05:00
export function resolveWorktreeReseedTargetPaths ( input : {
configPath : string ;
rootPath : string ;
} ) : WorktreeLocalPaths {
const envEntries = readPaperclipEnvEntries ( resolvePaperclipEnvFile ( input . configPath ) ) ;
const homeDir = nonEmpty ( envEntries . PAPERCLIP_HOME ) ;
const instanceId = nonEmpty ( envEntries . PAPERCLIP_INSTANCE_ID ) ;
if ( ! homeDir || ! instanceId ) {
throw new Error (
` Target config ${ input . configPath } does not look like a worktree-local Paperclip instance. Expected PAPERCLIP_HOME and PAPERCLIP_INSTANCE_ID in the adjacent .env. ` ,
) ;
}
return resolveWorktreeLocalPaths ( {
cwd : input.rootPath ,
homeDir ,
instanceId ,
} ) ;
}
[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
function resolveExistingGitWorktree ( selector : string , cwd : string ) : MergeSourceChoice | null {
const trimmed = selector . trim ( ) ;
if ( trimmed . length === 0 ) return null ;
const directPath = path . resolve ( trimmed ) ;
if ( existsSync ( directPath ) ) {
return {
worktree : directPath ,
branch : null ,
branchLabel : path.basename ( directPath ) ,
hasPaperclipConfig : existsSync ( path . resolve ( directPath , ".paperclip" , "config.json" ) ) ,
isCurrent : directPath === path . resolve ( cwd ) ,
} ;
}
return toMergeSourceChoices ( cwd ) . find ( ( choice ) = >
choice . worktree === directPath
|| path . basename ( choice . worktree ) === trimmed
|| choice . branchLabel === trimmed
|| choice . branch === trimmed ,
) ? ? null ;
}
async function ensureRepairTargetWorktree ( input : {
selector? : string ;
seedMode : WorktreeSeedMode ;
opts : WorktreeRepairOptions ;
} ) : Promise < ResolvedWorktreeRepairTarget | null > {
const cwd = process . cwd ( ) ;
const currentRoot = path . resolve ( cwd ) ;
const currentConfigPath = path . resolve ( currentRoot , ".paperclip" , "config.json" ) ;
if ( ! input . selector ) {
if ( isPrimaryGitWorktree ( cwd ) ) {
return null ;
}
return {
rootPath : currentRoot ,
configPath : currentConfigPath ,
label : path.basename ( currentRoot ) ,
branchName : detectGitBranchName ( cwd ) ,
created : false ,
} ;
}
const existing = resolveExistingGitWorktree ( input . selector , cwd ) ;
if ( existing ) {
return {
rootPath : existing.worktree ,
configPath : path.resolve ( existing . worktree , ".paperclip" , "config.json" ) ,
label : existing.branchLabel ,
branchName : existing.branchLabel === "(detached)" ? null : existing . branchLabel ,
created : false ,
} ;
}
const repoRoot = resolvePrimaryGitRepoRoot ( cwd ) ;
const branchName = validateGitBranchName ( repoRoot , input . selector ) ;
const targetPath = path . resolve (
repoRoot ,
".paperclip" ,
"worktrees" ,
resolveRepairWorktreeDirName ( branchName ) ,
) ;
if ( existsSync ( targetPath ) ) {
throw new Error ( ` Target path already exists but is not a registered git worktree: ${ targetPath } ` ) ;
}
mkdirSync ( path . dirname ( targetPath ) , { recursive : true } ) ;
const spinner = p . spinner ( ) ;
spinner . start ( ` Creating git worktree for ${ branchName } ... ` ) ;
try {
execFileSync ( "git" , resolveGitWorktreeAddArgs ( {
branchName ,
targetPath ,
branchExists : localBranchExists ( repoRoot , branchName ) ,
} ) , {
cwd : repoRoot ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
spinner . stop ( ` Created git worktree at ${ targetPath } . ` ) ;
} catch ( error ) {
spinner . stop ( pc . red ( "Failed to create git worktree." ) ) ;
throw new Error ( extractExecSyncErrorMessage ( error ) ? ? String ( error ) ) ;
}
installDependenciesBestEffort ( targetPath ) ;
return {
rootPath : targetPath ,
configPath : path.resolve ( targetPath , ".paperclip" , "config.json" ) ,
label : branchName ,
branchName ,
created : true ,
} ;
}
2026-03-10 10:08:13 -05:00
function resolveSourceConnectionString ( config : PaperclipConfig , envEntries : Record < string , string > , portOverride? : number ) : string {
if ( config . database . mode === "postgres" ) {
const connectionString = nonEmpty ( envEntries . DATABASE_URL ) ? ? nonEmpty ( config . database . connectionString ) ;
if ( ! connectionString ) {
throw new Error (
"Source instance uses postgres mode but has no connection string in config or adjacent .env." ,
) ;
}
return connectionString ;
}
const port = portOverride ? ? config . database . embeddedPostgresPort ;
return ` postgres://paperclip:paperclip@127.0.0.1: ${ port } /paperclip ` ;
}
2026-03-10 12:57:53 -05:00
export function copySeededSecretsKey ( input : {
sourceConfigPath : string ;
sourceConfig : PaperclipConfig ;
sourceEnvEntries : Record < string , string > ;
targetKeyFilePath : string ;
} ) : void {
if ( input . sourceConfig . secrets . provider !== "local_encrypted" ) {
return ;
}
mkdirSync ( path . dirname ( input . targetKeyFilePath ) , { recursive : true } ) ;
2026-03-11 16:38:31 -05:00
const allowProcessEnvFallback = isCurrentSourceConfigPath ( input . sourceConfigPath ) ;
2026-03-10 12:57:53 -05:00
const sourceInlineMasterKey =
nonEmpty ( input . sourceEnvEntries . PAPERCLIP_SECRETS_MASTER_KEY ) ? ?
2026-03-11 16:38:31 -05:00
( allowProcessEnvFallback ? nonEmpty ( process . env . PAPERCLIP_SECRETS_MASTER_KEY ) : null ) ;
2026-03-10 12:57:53 -05:00
if ( sourceInlineMasterKey ) {
writeFileSync ( input . targetKeyFilePath , sourceInlineMasterKey , {
encoding : "utf8" ,
mode : 0o600 ,
} ) ;
try {
chmodSync ( input . targetKeyFilePath , 0 o600 ) ;
} catch {
// best effort
}
return ;
}
const sourceKeyFileOverride =
nonEmpty ( input . sourceEnvEntries . PAPERCLIP_SECRETS_MASTER_KEY_FILE ) ? ?
2026-03-11 16:38:31 -05:00
( allowProcessEnvFallback ? nonEmpty ( process . env . PAPERCLIP_SECRETS_MASTER_KEY_FILE ) : null ) ;
2026-03-10 12:57:53 -05:00
const sourceConfiguredKeyPath = sourceKeyFileOverride ? ? input . sourceConfig . secrets . localEncrypted . keyFilePath ;
const sourceKeyFilePath = resolveRuntimeLikePath ( sourceConfiguredKeyPath , input . sourceConfigPath ) ;
if ( ! existsSync ( sourceKeyFilePath ) ) {
throw new Error (
` Cannot seed worktree database because source local_encrypted secrets key was not found at ${ sourceKeyFilePath } . ` ,
) ;
}
copyFileSync ( sourceKeyFilePath , input . targetKeyFilePath ) ;
try {
chmodSync ( input . targetKeyFilePath , 0 o600 ) ;
} catch {
// best effort
}
}
2026-03-10 10:08:13 -05:00
async function ensureEmbeddedPostgres ( dataDir : string , preferredPort : number ) : Promise < EmbeddedPostgresHandle > {
const moduleName = "embedded-postgres" ;
let EmbeddedPostgres : EmbeddedPostgresCtor ;
try {
const mod = await import ( moduleName ) ;
EmbeddedPostgres = mod . default as EmbeddedPostgresCtor ;
} catch {
throw new Error (
"Embedded PostgreSQL support requires dependency `embedded-postgres`. Reinstall dependencies and try again." ,
) ;
}
const postmasterPidFile = path . resolve ( dataDir , "postmaster.pid" ) ;
const runningPid = readRunningPostmasterPid ( postmasterPidFile ) ;
if ( runningPid ) {
return {
port : readPidFilePort ( postmasterPidFile ) ? ? preferredPort ,
startedByThisProcess : false ,
stop : async ( ) = > { } ,
} ;
}
const port = await findAvailablePort ( preferredPort ) ;
2026-03-26 11:34:16 -05:00
const logBuffer = createEmbeddedPostgresLogBuffer ( ) ;
2026-03-10 10:08:13 -05:00
const instance = new EmbeddedPostgres ( {
databaseDir : dataDir ,
user : "paperclip" ,
password : "paperclip" ,
port ,
persistent : true ,
2026-03-24 08:03:04 -05:00
initdbFlags : [ "--encoding=UTF8" , "--locale=C" , "--lc-messages=C" ] ,
2026-03-26 11:34:16 -05:00
onLog : logBuffer.append ,
onError : logBuffer.append ,
2026-03-10 10:08:13 -05:00
} ) ;
if ( ! existsSync ( path . resolve ( dataDir , "PG_VERSION" ) ) ) {
2026-03-26 11:34:16 -05:00
try {
await instance . initialise ( ) ;
} catch ( error ) {
throw formatEmbeddedPostgresError ( error , {
fallbackMessage : ` Failed to initialize embedded PostgreSQL cluster in ${ dataDir } on port ${ port } ` ,
recentLogs : logBuffer.getRecentLogs ( ) ,
} ) ;
}
2026-03-10 10:08:13 -05:00
}
if ( existsSync ( postmasterPidFile ) ) {
rmSync ( postmasterPidFile , { force : true } ) ;
}
2026-03-26 11:34:16 -05:00
try {
await instance . start ( ) ;
} catch ( error ) {
throw formatEmbeddedPostgresError ( error , {
fallbackMessage : ` Failed to start embedded PostgreSQL on port ${ port } ` ,
recentLogs : logBuffer.getRecentLogs ( ) ,
} ) ;
}
2026-03-10 10:08:13 -05:00
return {
port ,
startedByThisProcess : true ,
stop : async ( ) = > {
await instance . stop ( ) ;
} ,
} ;
}
2026-04-10 22:26:30 -05:00
export async function pauseSeededScheduledRoutines ( connectionString : string ) : Promise < number > {
const db = createDb ( connectionString ) ;
try {
const scheduledRoutineIds = await db
. selectDistinct ( { routineId : routineTriggers.routineId } )
. from ( routineTriggers )
. where ( and ( eq ( routineTriggers . kind , "schedule" ) , eq ( routineTriggers . enabled , true ) ) ) ;
const idsToPause = scheduledRoutineIds
. map ( ( row ) = > row . routineId )
. filter ( ( value ) : value is string = > Boolean ( value ) ) ;
if ( idsToPause . length === 0 ) {
return 0 ;
}
const paused = await db
. update ( routines )
. set ( {
status : "paused" ,
updatedAt : new Date ( ) ,
} )
. where ( and ( inArray ( routines . id , idsToPause ) , sql ` ${ routines . status } <> 'paused' ` , sql ` ${ routines . status } <> 'archived' ` ) )
. returning ( { id : routines.id } ) ;
return paused . length ;
} finally {
await db . $client ? . end ? . ( { timeout : 5 } ) . catch ( ( ) = > undefined ) ;
}
}
[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
const EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY : SeededWorktreeExecutionQuarantineSummary = {
disabledTimerHeartbeats : 0 ,
resetRunningAgents : 0 ,
quarantinedInProgressIssues : 0 ,
unassignedTodoIssues : 0 ,
unassignedReviewIssues : 0 ,
} ;
function isRecord ( value : unknown ) : value is Record < string , unknown > {
return Boolean ( value ) && typeof value === "object" && ! Array . isArray ( value ) ;
}
function isEnabledValue ( value : unknown ) : boolean {
return value === true || value === "true" || value === 1 || value === "1" ;
}
function normalizeWorktreeRuntimeConfig ( runtimeConfig : unknown ) : {
runtimeConfig : Record < string , unknown > ;
disabledTimerHeartbeat : boolean ;
changed : boolean ;
} {
const nextRuntimeConfig = isRecord ( runtimeConfig ) ? { . . . runtimeConfig } : { } ;
const heartbeat = isRecord ( nextRuntimeConfig . heartbeat ) ? { . . . nextRuntimeConfig . heartbeat } : null ;
if ( ! heartbeat ) {
return { runtimeConfig : nextRuntimeConfig , disabledTimerHeartbeat : false , changed : false } ;
}
const disabledTimerHeartbeat = isEnabledValue ( heartbeat . enabled ) ;
if ( heartbeat . enabled !== false ) {
heartbeat . enabled = false ;
nextRuntimeConfig . heartbeat = heartbeat ;
return { runtimeConfig : nextRuntimeConfig , disabledTimerHeartbeat , changed : true } ;
}
return { runtimeConfig : nextRuntimeConfig , disabledTimerHeartbeat : false , changed : false } ;
}
export async function quarantineSeededWorktreeExecutionState (
connectionString : string ,
) : Promise < SeededWorktreeExecutionQuarantineSummary > {
const db = createDb ( connectionString ) ;
const summary = { . . . EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY } ;
try {
await db . transaction ( async ( tx ) = > {
const seededAgents = await tx
. select ( {
id : agents.id ,
status : agents.status ,
runtimeConfig : agents.runtimeConfig ,
} )
. from ( agents ) ;
for ( const agent of seededAgents ) {
const normalized = normalizeWorktreeRuntimeConfig ( agent . runtimeConfig ) ;
const nextStatus = agent . status === "running" ? "idle" : agent . status ;
if ( normalized . disabledTimerHeartbeat ) {
summary . disabledTimerHeartbeats += 1 ;
}
if ( agent . status === "running" ) {
summary . resetRunningAgents += 1 ;
}
if ( normalized . changed || nextStatus !== agent . status ) {
await tx
. update ( agents )
. set ( {
runtimeConfig : normalized.runtimeConfig ,
status : nextStatus ,
updatedAt : new Date ( ) ,
} )
. where ( eq ( agents . id , agent . id ) ) ;
}
}
const affectedIssues = await tx
. select ( {
id : issues.id ,
companyId : issues.companyId ,
status : issues.status ,
} )
. from ( issues )
. where (
and (
sql ` ${ issues . assigneeAgentId } is not null ` ,
sql ` ${ issues . assigneeUserId } is null ` ,
inArray ( issues . status , [ "todo" , "in_progress" , "in_review" ] ) ,
) ,
) ;
for ( const issue of affectedIssues ) {
const nextStatus = issue . status === "in_progress" ? "blocked" : issue . status ;
await tx
. update ( issues )
. set ( {
status : nextStatus ,
assigneeAgentId : null ,
checkoutRunId : null ,
executionRunId : null ,
executionAgentNameKey : null ,
executionLockedAt : null ,
executionWorkspaceId : null ,
updatedAt : new Date ( ) ,
} )
. where ( eq ( issues . id , issue . id ) ) ;
if ( issue . status === "in_progress" ) {
summary . quarantinedInProgressIssues += 1 ;
await tx . insert ( issueComments ) . values ( {
companyId : issue.companyId ,
issueId : issue.id ,
body :
"Quarantined during worktree seed so copied in-flight work does not auto-run in this isolated instance. " +
"Reassign or unblock here only if you intentionally want the worktree instance to own this task." ,
} ) ;
} else if ( issue . status === "todo" ) {
summary . unassignedTodoIssues += 1 ;
} else if ( issue . status === "in_review" ) {
summary . unassignedReviewIssues += 1 ;
}
}
} ) ;
return summary ;
} finally {
await db . $client ? . end ? . ( { timeout : 5 } ) . catch ( ( ) = > undefined ) ;
}
}
2026-03-10 10:08:13 -05:00
async function seedWorktreeDatabase ( input : {
sourceConfigPath : string ;
sourceConfig : PaperclipConfig ;
targetConfig : PaperclipConfig ;
targetPaths : WorktreeLocalPaths ;
instanceId : string ;
2026-03-10 07:41:01 -05:00
seedMode : WorktreeSeedMode ;
[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
preserveLiveWork? : boolean ;
2026-03-10 13:50:29 -05:00
} ) : Promise < SeedWorktreeDatabaseResult > {
2026-03-10 07:41:01 -05:00
const seedPlan = resolveWorktreeSeedPlan ( input . seedMode ) ;
2026-03-10 10:08:13 -05:00
const sourceEnvFile = resolvePaperclipEnvFile ( input . sourceConfigPath ) ;
const sourceEnvEntries = readPaperclipEnvEntries ( sourceEnvFile ) ;
2026-03-10 12:57:53 -05:00
copySeededSecretsKey ( {
sourceConfigPath : input.sourceConfigPath ,
sourceConfig : input.sourceConfig ,
sourceEnvEntries ,
targetKeyFilePath : input.targetPaths.secretsKeyFilePath ,
} ) ;
2026-03-10 10:08:13 -05:00
let sourceHandle : EmbeddedPostgresHandle | null = null ;
let targetHandle : EmbeddedPostgresHandle | null = null ;
try {
if ( input . sourceConfig . database . mode === "embedded-postgres" ) {
sourceHandle = await ensureEmbeddedPostgres (
input . sourceConfig . database . embeddedPostgresDataDir ,
input . sourceConfig . database . embeddedPostgresPort ,
) ;
2026-04-09 06:12:29 -05:00
const sourceAdminConnectionString = ` postgres://paperclip:paperclip@127.0.0.1: ${ sourceHandle . port } /postgres ` ;
await ensurePostgresDatabase ( sourceAdminConnectionString , "paperclip" ) ;
2026-03-10 10:08:13 -05:00
}
const sourceConnectionString = resolveSourceConnectionString (
input . sourceConfig ,
sourceEnvEntries ,
sourceHandle ? . port ,
) ;
const backup = await runDatabaseBackup ( {
connectionString : sourceConnectionString ,
backupDir : path.resolve ( input . targetPaths . backupDir , "seed" ) ,
2026-04-07 09:54:39 +02:00
retention : { dailyDays : 7 , weeklyWeeks : 4 , monthlyMonths : 1 } ,
2026-03-10 10:08:13 -05:00
filenamePrefix : ` ${ input . instanceId } -seed ` ,
includeMigrationJournal : true ,
2026-03-10 07:41:01 -05:00
excludeTables : seedPlan.excludedTables ,
nullifyColumns : seedPlan.nullifyColumns ,
2026-03-10 10:08:13 -05:00
} ) ;
targetHandle = await ensureEmbeddedPostgres (
input . targetConfig . database . embeddedPostgresDataDir ,
input . targetConfig . database . embeddedPostgresPort ,
) ;
const adminConnectionString = ` postgres://paperclip:paperclip@127.0.0.1: ${ targetHandle . port } /postgres ` ;
await ensurePostgresDatabase ( adminConnectionString , "paperclip" ) ;
const targetConnectionString = ` postgres://paperclip:paperclip@127.0.0.1: ${ targetHandle . port } /paperclip ` ;
await runDatabaseRestore ( {
connectionString : targetConnectionString ,
backupFile : backup.backupFile ,
} ) ;
2026-03-10 10:08:58 -05:00
await applyPendingMigrations ( targetConnectionString ) ;
[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
const executionQuarantine = input . preserveLiveWork
? { . . . EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY }
: await quarantineSeededWorktreeExecutionState ( targetConnectionString ) ;
const pausedScheduledRoutines = await pauseSeededScheduledRoutines ( targetConnectionString ) ;
2026-03-10 13:50:29 -05:00
const reboundWorkspaces = await rebindSeededProjectWorkspaces ( {
targetConnectionString ,
currentCwd : input.targetPaths.cwd ,
} ) ;
2026-03-10 10:08:13 -05:00
2026-03-10 13:50:29 -05:00
return {
backupSummary : formatDatabaseBackupResult ( backup ) ,
[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
pausedScheduledRoutines ,
executionQuarantine ,
2026-03-10 13:50:29 -05:00
reboundWorkspaces ,
} ;
2026-03-10 10:08:13 -05:00
} finally {
if ( targetHandle ? . startedByThisProcess ) {
await targetHandle . stop ( ) ;
}
if ( sourceHandle ? . startedByThisProcess ) {
await sourceHandle . stop ( ) ;
}
}
}
2026-03-10 16:52:26 -05:00
async function runWorktreeInit ( opts : WorktreeInitOptions ) : Promise < void > {
2026-03-10 10:08:13 -05:00
const cwd = process . cwd ( ) ;
2026-03-13 11:12:43 -05:00
const worktreeName = resolveSuggestedWorktreeName (
2026-03-10 10:08:13 -05:00
cwd ,
opts . name ? ? detectGitBranchName ( cwd ) ? ? undefined ,
) ;
2026-03-10 07:41:01 -05:00
const seedMode = opts . seedMode ? ? "minimal" ;
if ( ! isWorktreeSeedMode ( seedMode ) ) {
throw new Error ( ` Unsupported seed mode " ${ seedMode } ". Expected one of: minimal, full. ` ) ;
}
2026-03-13 11:12:43 -05:00
const instanceId = sanitizeWorktreeInstanceId ( opts . instance ? ? worktreeName ) ;
2026-03-10 10:08:13 -05:00
const paths = resolveWorktreeLocalPaths ( {
cwd ,
2026-03-13 07:24:39 -05:00
homeDir : resolveWorktreeHome ( opts . home ) ,
2026-03-10 10:08:13 -05:00
instanceId ,
} ) ;
2026-03-13 11:12:43 -05:00
const branding = {
2026-04-07 17:02:34 -05:00
name : opts.name ? ? worktreeName ,
color : opts.color ? ? generateWorktreeColor ( ) ,
2026-03-13 11:12:43 -05:00
} ;
2026-03-10 10:08:13 -05:00
const sourceConfigPath = resolveSourceConfigPath ( opts ) ;
const sourceConfig = existsSync ( sourceConfigPath ) ? readConfig ( sourceConfigPath ) : null ;
if ( ( existsSync ( paths . configPath ) || existsSync ( paths . instanceRoot ) ) && ! opts . force ) {
throw new Error (
` Worktree config already exists at ${ paths . configPath } or instance data exists at ${ paths . instanceRoot } . Re-run with --force to replace it. ` ,
) ;
}
if ( opts . force ) {
rmSync ( paths . repoConfigDir , { recursive : true , force : true } ) ;
rmSync ( paths . instanceRoot , { recursive : true , force : true } ) ;
}
2026-03-26 10:56:13 -05:00
const claimedPorts = collectClaimedWorktreePorts ( paths . homeDir , paths . instanceId , paths . cwd ) ;
2026-03-10 10:08:13 -05:00
const preferredServerPort = opts . serverPort ? ? ( ( sourceConfig ? . server . port ? ? 3100 ) + 1 ) ;
2026-03-26 10:56:13 -05:00
const serverPort = await findAvailablePort ( preferredServerPort , claimedPorts . serverPorts ) ;
2026-03-10 10:08:13 -05:00
const preferredDbPort = opts . dbPort ? ? ( ( sourceConfig ? . database . embeddedPostgresPort ? ? 54329 ) + 1 ) ;
2026-03-26 10:56:13 -05:00
const databasePort = await findAvailablePort (
preferredDbPort ,
new Set ( [ . . . claimedPorts . databasePorts , serverPort ] ) ,
) ;
2026-03-10 10:08:13 -05:00
const targetConfig = buildWorktreeConfig ( {
sourceConfig ,
paths ,
serverPort ,
databasePort ,
} ) ;
writeConfig ( targetConfig , paths . configPath ) ;
2026-03-11 16:38:16 -05:00
const sourceEnvEntries = readPaperclipEnvEntries ( resolvePaperclipEnvFile ( sourceConfigPath ) ) ;
const existingAgentJwtSecret =
nonEmpty ( sourceEnvEntries . PAPERCLIP_AGENT_JWT_SECRET ) ? ?
nonEmpty ( process . env . PAPERCLIP_AGENT_JWT_SECRET ) ;
mergePaperclipEnvEntries (
{
2026-03-13 11:12:43 -05:00
. . . buildWorktreeEnvEntries ( paths , branding ) ,
2026-03-11 16:38:16 -05:00
. . . ( existingAgentJwtSecret ? { PAPERCLIP_AGENT_JWT_SECRET : existingAgentJwtSecret } : { } ) ,
} ,
paths . envPath ,
) ;
2026-03-10 10:08:13 -05:00
ensureAgentJwtSecret ( paths . configPath ) ;
loadPaperclipEnvFile ( paths . configPath ) ;
2026-03-10 14:55:35 -05:00
const copiedGitHooks = copyGitHooksToWorktreeGitDir ( cwd ) ;
2026-03-10 10:08:13 -05:00
let seedSummary : string | null = null ;
[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
let seedExecutionQuarantineSummary : SeededWorktreeExecutionQuarantineSummary | null = null ;
let pausedScheduledRoutineCount : number | null = null ;
2026-03-10 13:50:29 -05:00
let reboundWorkspaceSummary : SeedWorktreeDatabaseResult [ "reboundWorkspaces" ] = [ ] ;
2026-03-10 10:08:13 -05:00
if ( opts . seed !== false ) {
if ( ! sourceConfig ) {
throw new Error (
` Cannot seed worktree database because source config was not found at ${ sourceConfigPath } . Use --no-seed or provide --from-config. ` ,
) ;
}
const spinner = p . spinner ( ) ;
2026-03-10 07:41:01 -05:00
spinner . start ( ` Seeding isolated worktree database from source instance ( ${ seedMode } )... ` ) ;
2026-03-10 10:08:13 -05:00
try {
2026-03-10 13:50:29 -05:00
const seeded = await seedWorktreeDatabase ( {
2026-03-10 10:08:13 -05:00
sourceConfigPath ,
sourceConfig ,
targetConfig ,
targetPaths : paths ,
instanceId ,
2026-03-10 07:41:01 -05:00
seedMode ,
[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
preserveLiveWork : opts.preserveLiveWork ,
2026-03-10 10:08:13 -05:00
} ) ;
2026-03-10 13:50:29 -05:00
seedSummary = seeded . backupSummary ;
[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
seedExecutionQuarantineSummary = seeded . executionQuarantine ;
pausedScheduledRoutineCount = seeded . pausedScheduledRoutines ;
2026-03-10 13:50:29 -05:00
reboundWorkspaceSummary = seeded . reboundWorkspaces ;
2026-03-10 07:41:01 -05:00
spinner . stop ( ` Seeded isolated worktree database ( ${ seedMode } ). ` ) ;
2026-03-10 10:08:13 -05:00
} catch ( error ) {
spinner . stop ( pc . red ( "Failed to seed worktree database." ) ) ;
throw error ;
}
}
p . log . message ( pc . dim ( ` Repo config: ${ paths . configPath } ` ) ) ;
p . log . message ( pc . dim ( ` Repo env: ${ paths . envPath } ` ) ) ;
p . log . message ( pc . dim ( ` Isolated home: ${ paths . homeDir } ` ) ) ;
p . log . message ( pc . dim ( ` Instance: ${ paths . instanceId } ` ) ) ;
2026-03-13 11:12:43 -05:00
p . log . message ( pc . dim ( ` Worktree badge: ${ branding . name } ( ${ branding . color } ) ` ) ) ;
2026-03-10 10:08:13 -05:00
p . log . message ( pc . dim ( ` Server port: ${ serverPort } | DB port: ${ databasePort } ` ) ) ;
2026-03-10 14:55:35 -05:00
if ( copiedGitHooks ? . copied ) {
p . log . message (
pc . dim ( ` Mirrored git hooks: ${ copiedGitHooks . sourceHooksPath } -> ${ copiedGitHooks . targetHooksPath } ` ) ,
) ;
}
2026-03-10 10:08:13 -05:00
if ( seedSummary ) {
2026-03-10 07:41:01 -05:00
p . log . message ( pc . dim ( ` Seed mode: ${ seedMode } ` ) ) ;
2026-03-10 10:08:13 -05:00
p . log . message ( pc . dim ( ` Seed snapshot: ${ seedSummary } ` ) ) ;
[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
if ( opts . preserveLiveWork ) {
p . log . warning ( "Preserved copied live work; this worktree instance may auto-run source-instance assignments." ) ;
} else if ( seedExecutionQuarantineSummary ) {
p . log . message (
pc . dim ( ` Seed execution quarantine: ${ formatSeededWorktreeExecutionQuarantineSummary ( seedExecutionQuarantineSummary ) } ` ) ,
) ;
}
if ( pausedScheduledRoutineCount != null ) {
p . log . message ( pc . dim ( ` Paused scheduled routines: ${ pausedScheduledRoutineCount } ` ) ) ;
}
2026-03-10 13:50:29 -05:00
for ( const rebound of reboundWorkspaceSummary ) {
p . log . message (
pc . dim ( ` Rebound workspace ${ rebound . name } : ${ rebound . fromCwd } -> ${ rebound . toCwd } ` ) ,
) ;
}
2026-03-10 10:08:13 -05:00
}
p . outro (
pc . green (
` Worktree ready. Run Paperclip inside this repo and the CLI/server will use ${ paths . instanceId } automatically. ` ,
) ,
) ;
}
2026-03-10 16:52:26 -05:00
export async function worktreeInitCommand ( opts : WorktreeInitOptions ) : Promise < void > {
printPaperclipCliBanner ( ) ;
p . intro ( pc . bgCyan ( pc . black ( " paperclipai worktree init " ) ) ) ;
await runWorktreeInit ( opts ) ;
}
export async function worktreeMakeCommand ( nameArg : string , opts : WorktreeMakeOptions ) : Promise < void > {
printPaperclipCliBanner ( ) ;
p . intro ( pc . bgCyan ( pc . black ( " paperclipai worktree:make " ) ) ) ;
const name = resolveWorktreeMakeName ( nameArg ) ;
2026-03-13 07:24:39 -05:00
const startPoint = resolveWorktreeStartPoint ( opts . startPoint ) ;
2026-03-10 16:52:26 -05:00
const sourceCwd = process . cwd ( ) ;
2026-03-13 14:24:06 -05:00
const sourceConfigPath = resolveSourceConfigPath ( opts ) ;
2026-03-10 16:52:26 -05:00
const targetPath = resolveWorktreeMakeTargetPath ( name ) ;
if ( existsSync ( targetPath ) ) {
throw new Error ( ` Target path already exists: ${ targetPath } ` ) ;
}
mkdirSync ( path . dirname ( targetPath ) , { recursive : true } ) ;
2026-03-13 07:24:39 -05:00
if ( startPoint ) {
const [ remote ] = startPoint . split ( "/" , 1 ) ;
2026-03-11 09:15:27 -05:00
try {
execFileSync ( "git" , [ "fetch" , remote ] , {
cwd : sourceCwd ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
} catch ( error ) {
throw new Error (
` Failed to fetch from remote " ${ remote } ": ${ extractExecSyncErrorMessage ( error ) ? ? String ( error ) } ` ,
) ;
}
}
2026-03-10 16:52:26 -05:00
const worktreeArgs = resolveGitWorktreeAddArgs ( {
branchName : name ,
targetPath ,
2026-03-13 07:24:39 -05:00
branchExists : ! startPoint && localBranchExists ( sourceCwd , name ) ,
startPoint ,
2026-03-10 16:52:26 -05:00
} ) ;
const spinner = p . spinner ( ) ;
spinner . start ( ` Creating git worktree at ${ targetPath } ... ` ) ;
try {
execFileSync ( "git" , worktreeArgs , {
cwd : sourceCwd ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
spinner . stop ( ` Created git worktree at ${ targetPath } . ` ) ;
} catch ( error ) {
spinner . stop ( pc . red ( "Failed to create git worktree." ) ) ;
throw new Error ( extractExecSyncErrorMessage ( error ) ? ? String ( error ) ) ;
}
[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
installDependenciesBestEffort ( targetPath ) ;
2026-03-11 09:31:41 -05:00
2026-03-10 16:52:26 -05:00
const originalCwd = process . cwd ( ) ;
try {
process . chdir ( targetPath ) ;
await runWorktreeInit ( {
. . . opts ,
name ,
2026-03-13 14:24:06 -05:00
sourceConfigPathOverride : sourceConfigPath ,
2026-03-10 16:52:26 -05:00
} ) ;
} catch ( error ) {
throw error ;
} finally {
process . chdir ( originalCwd ) ;
}
}
[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
function installDependenciesBestEffort ( targetPath : string ) : void {
const installSpinner = p . spinner ( ) ;
installSpinner . start ( "Installing dependencies..." ) ;
try {
execFileSync ( "pnpm" , [ "install" ] , {
cwd : targetPath ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
installSpinner . stop ( "Installed dependencies." ) ;
} catch ( error ) {
installSpinner . stop ( pc . yellow ( "Failed to install dependencies (continuing anyway)." ) ) ;
p . log . warning ( extractExecSyncErrorMessage ( error ) ? ? String ( error ) ) ;
}
}
2026-03-13 07:24:39 -05:00
type WorktreeCleanupOptions = {
instance? : string ;
home? : string ;
force? : boolean ;
} ;
type GitWorktreeListEntry = {
worktree : string ;
branch : string | null ;
bare : boolean ;
detached : boolean ;
} ;
2026-03-20 15:17:51 -05:00
type MergeSourceChoice = {
worktree : string ;
branch : string | null ;
branchLabel : string ;
hasPaperclipConfig : boolean ;
isCurrent : boolean ;
} ;
2026-03-20 15:39:02 -05:00
type ResolvedWorktreeEndpoint = {
rootPath : string ;
configPath : string ;
label : string ;
isCurrent : boolean ;
} ;
2026-04-07 07:48:22 -05:00
type ResolvedWorktreeReseedSource = {
configPath : string ;
label : string ;
} ;
[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
type ResolvedWorktreeRepairTarget = {
rootPath : string ;
configPath : string ;
label : string ;
branchName : string | null ;
created : boolean ;
} ;
2026-03-13 07:24:39 -05:00
function parseGitWorktreeList ( cwd : string ) : GitWorktreeListEntry [ ] {
const raw = execFileSync ( "git" , [ "worktree" , "list" , "--porcelain" ] , {
cwd ,
encoding : "utf8" ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
const entries : GitWorktreeListEntry [ ] = [ ] ;
let current : Partial < GitWorktreeListEntry > = { } ;
for ( const line of raw . split ( "\n" ) ) {
if ( line . startsWith ( "worktree " ) ) {
current = { worktree : line.slice ( "worktree " . length ) } ;
} else if ( line . startsWith ( "branch " ) ) {
current . branch = line . slice ( "branch " . length ) ;
} else if ( line === "bare" ) {
current . bare = true ;
} else if ( line === "detached" ) {
current . detached = true ;
} else if ( line === "" && current . worktree ) {
entries . push ( {
worktree : current.worktree ,
branch : current.branch ? ? null ,
bare : current.bare ? ? false ,
detached : current.detached ? ? false ,
} ) ;
current = { } ;
}
}
if ( current . worktree ) {
entries . push ( {
worktree : current.worktree ,
branch : current.branch ? ? null ,
bare : current.bare ? ? false ,
detached : current.detached ? ? false ,
} ) ;
}
return entries ;
}
2026-03-20 15:17:51 -05:00
function toMergeSourceChoices ( cwd : string ) : MergeSourceChoice [ ] {
const currentCwd = path . resolve ( cwd ) ;
return parseGitWorktreeList ( cwd ) . map ( ( entry ) = > {
const branchLabel = entry . branch ? . replace ( /^refs\/heads\// , "" ) ? ? "(detached)" ;
const worktreePath = path . resolve ( entry . worktree ) ;
return {
worktree : worktreePath ,
branch : entry.branch ,
branchLabel ,
hasPaperclipConfig : existsSync ( path . resolve ( worktreePath , ".paperclip" , "config.json" ) ) ,
isCurrent : worktreePath === currentCwd ,
} ;
} ) ;
}
2026-03-13 07:24:39 -05:00
function branchHasUniqueCommits ( cwd : string , branchName : string ) : boolean {
try {
const output = execFileSync (
"git" ,
[ "log" , "--oneline" , branchName , "--not" , "--remotes" , "--exclude" , ` refs/heads/ ${ branchName } ` , "--branches" ] ,
{ cwd , encoding : "utf8" , stdio : [ "ignore" , "pipe" , "pipe" ] } ,
) . trim ( ) ;
return output . length > 0 ;
} catch {
return false ;
}
}
function branchExistsOnAnyRemote ( cwd : string , branchName : string ) : boolean {
try {
const output = execFileSync (
"git" ,
[ "branch" , "-r" , "--list" , ` */ ${ branchName } ` ] ,
{ cwd , encoding : "utf8" , stdio : [ "ignore" , "pipe" , "pipe" ] } ,
) . trim ( ) ;
return output . length > 0 ;
} catch {
return false ;
}
}
function worktreePathHasUncommittedChanges ( worktreePath : string ) : boolean {
try {
const output = execFileSync (
"git" ,
[ "status" , "--porcelain" ] ,
{ cwd : worktreePath , encoding : "utf8" , stdio : [ "ignore" , "pipe" , "pipe" ] } ,
) . trim ( ) ;
return output . length > 0 ;
} catch {
return false ;
}
}
export async function worktreeCleanupCommand ( nameArg : string , opts : WorktreeCleanupOptions ) : Promise < void > {
printPaperclipCliBanner ( ) ;
p . intro ( pc . bgCyan ( pc . black ( " paperclipai worktree:cleanup " ) ) ) ;
const name = resolveWorktreeMakeName ( nameArg ) ;
const sourceCwd = process . cwd ( ) ;
const targetPath = resolveWorktreeMakeTargetPath ( name ) ;
const instanceId = sanitizeWorktreeInstanceId ( opts . instance ? ? name ) ;
const homeDir = path . resolve ( expandHomePrefix ( resolveWorktreeHome ( opts . home ) ) ) ;
const instanceRoot = path . resolve ( homeDir , "instances" , instanceId ) ;
// ── 1. Assess current state ──────────────────────────────────────────
const hasBranch = localBranchExists ( sourceCwd , name ) ;
const hasTargetDir = existsSync ( targetPath ) ;
const hasInstanceData = existsSync ( instanceRoot ) ;
const worktrees = parseGitWorktreeList ( sourceCwd ) ;
const linkedWorktree = worktrees . find (
( wt ) = > wt . branch === ` refs/heads/ ${ name } ` || path . resolve ( wt . worktree ) === path . resolve ( targetPath ) ,
) ;
if ( ! hasBranch && ! hasTargetDir && ! hasInstanceData && ! linkedWorktree ) {
p . log . info ( "Nothing to clean up — no branch, worktree directory, or instance data found." ) ;
p . outro ( pc . green ( "Already clean." ) ) ;
return ;
}
// ── 2. Safety checks ────────────────────────────────────────────────
const problems : string [ ] = [ ] ;
if ( hasBranch && branchHasUniqueCommits ( sourceCwd , name ) ) {
const onRemote = branchExistsOnAnyRemote ( sourceCwd , name ) ;
if ( onRemote ) {
p . log . info (
` Branch " ${ name } " has unique local commits, but the branch also exists on a remote — safe to delete locally. ` ,
) ;
} else {
problems . push (
` Branch " ${ name } " has commits not found on any other branch or remote. ` +
` Deleting it will lose work. Push it first, or use --force. ` ,
) ;
}
}
if ( hasTargetDir && worktreePathHasUncommittedChanges ( targetPath ) ) {
problems . push (
` Worktree directory ${ targetPath } has uncommitted changes. Commit or stash first, or use --force. ` ,
) ;
}
if ( problems . length > 0 && ! opts . force ) {
for ( const problem of problems ) {
p . log . error ( problem ) ;
}
throw new Error ( "Safety checks failed. Resolve the issues above or re-run with --force." ) ;
}
if ( problems . length > 0 && opts . force ) {
for ( const problem of problems ) {
p . log . warning ( ` Overridden by --force: ${ problem } ` ) ;
}
}
// ── 3. Clean up (idempotent steps) ──────────────────────────────────
// 3a. Remove the git worktree registration
if ( linkedWorktree ) {
const worktreeDirExists = existsSync ( linkedWorktree . worktree ) ;
const spinner = p . spinner ( ) ;
if ( worktreeDirExists ) {
spinner . start ( ` Removing git worktree at ${ linkedWorktree . worktree } ... ` ) ;
try {
const removeArgs = [ "worktree" , "remove" , linkedWorktree . worktree ] ;
if ( opts . force ) removeArgs . push ( "--force" ) ;
execFileSync ( "git" , removeArgs , {
cwd : sourceCwd ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
spinner . stop ( ` Removed git worktree at ${ linkedWorktree . worktree } . ` ) ;
} catch ( error ) {
spinner . stop ( pc . yellow ( ` Could not remove worktree cleanly, will prune instead. ` ) ) ;
p . log . warning ( extractExecSyncErrorMessage ( error ) ? ? String ( error ) ) ;
}
} else {
spinner . start ( "Pruning stale worktree entry..." ) ;
execFileSync ( "git" , [ "worktree" , "prune" ] , {
cwd : sourceCwd ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
spinner . stop ( "Pruned stale worktree entry." ) ;
}
} else {
// Even without a linked worktree, prune to clean up any orphaned entries
execFileSync ( "git" , [ "worktree" , "prune" ] , {
cwd : sourceCwd ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
}
// 3b. Remove the worktree directory if it still exists (e.g. partial creation)
if ( existsSync ( targetPath ) ) {
const spinner = p . spinner ( ) ;
spinner . start ( ` Removing worktree directory ${ targetPath } ... ` ) ;
rmSync ( targetPath , { recursive : true , force : true } ) ;
spinner . stop ( ` Removed worktree directory ${ targetPath } . ` ) ;
}
// 3c. Delete the local branch (now safe — worktree is gone)
if ( localBranchExists ( sourceCwd , name ) ) {
const spinner = p . spinner ( ) ;
spinner . start ( ` Deleting local branch " ${ name } "... ` ) ;
try {
const deleteFlag = opts . force ? "-D" : "-d" ;
execFileSync ( "git" , [ "branch" , deleteFlag , name ] , {
cwd : sourceCwd ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
} ) ;
spinner . stop ( ` Deleted local branch " ${ name } ". ` ) ;
} catch ( error ) {
spinner . stop ( pc . yellow ( ` Could not delete branch " ${ name } ". ` ) ) ;
p . log . warning ( extractExecSyncErrorMessage ( error ) ? ? String ( error ) ) ;
}
}
// 3d. Remove instance data
if ( existsSync ( instanceRoot ) ) {
const spinner = p . spinner ( ) ;
spinner . start ( ` Removing instance data at ${ instanceRoot } ... ` ) ;
rmSync ( instanceRoot , { recursive : true , force : true } ) ;
spinner . stop ( ` Removed instance data at ${ instanceRoot } . ` ) ;
}
p . outro ( pc . green ( "Cleanup complete." ) ) ;
}
2026-03-10 10:08:13 -05:00
export async function worktreeEnvCommand ( opts : WorktreeEnvOptions ) : Promise < void > {
const configPath = resolveConfigPath ( opts . config ) ;
const envPath = resolvePaperclipEnvFile ( configPath ) ;
const envEntries = readPaperclipEnvEntries ( envPath ) ;
const out = {
PAPERCLIP_CONFIG : configPath ,
. . . ( envEntries . PAPERCLIP_HOME ? { PAPERCLIP_HOME : envEntries.PAPERCLIP_HOME } : { } ) ,
. . . ( envEntries . PAPERCLIP_INSTANCE_ID ? { PAPERCLIP_INSTANCE_ID : envEntries.PAPERCLIP_INSTANCE_ID } : { } ) ,
. . . ( envEntries . PAPERCLIP_CONTEXT ? { PAPERCLIP_CONTEXT : envEntries.PAPERCLIP_CONTEXT } : { } ) ,
. . . envEntries ,
} ;
if ( opts . json ) {
console . log ( JSON . stringify ( out , null , 2 ) ) ;
return ;
}
console . log ( formatShellExports ( out ) ) ;
}
2026-03-20 15:02:24 -05:00
type ClosableDb = ReturnType < typeof createDb > & {
$client ? : { end ? : ( opts ? : { timeout? : number } ) = > Promise < void > } ;
} ;
type OpenDbHandle = {
db : ClosableDb ;
stop : ( ) = > Promise < void > ;
} ;
type ResolvedMergeCompany = {
id : string ;
name : string ;
issuePrefix : string ;
} ;
async function closeDb ( db : ClosableDb ) : Promise < void > {
await db . $client ? . end ? . ( { timeout : 5 } ) . catch ( ( ) = > undefined ) ;
}
2026-03-20 15:39:02 -05:00
function resolveCurrentEndpoint ( ) : ResolvedWorktreeEndpoint {
return {
rootPath : path.resolve ( process . cwd ( ) ) ,
configPath : resolveConfigPath ( ) ,
label : "current" ,
isCurrent : true ,
} ;
}
2026-03-20 16:12:10 -05:00
function resolveAttachmentLookupStorages ( input : {
sourceEndpoint : ResolvedWorktreeEndpoint ;
targetEndpoint : ResolvedWorktreeEndpoint ;
} ) : ConfiguredStorage [ ] {
const orderedConfigPaths = [
input . sourceEndpoint . configPath ,
resolveCurrentEndpoint ( ) . configPath ,
input . targetEndpoint . configPath ,
. . . toMergeSourceChoices ( process . cwd ( ) )
. filter ( ( choice ) = > choice . hasPaperclipConfig )
. map ( ( choice ) = > path . resolve ( choice . worktree , ".paperclip" , "config.json" ) ) ,
] ;
const seen = new Set < string > ( ) ;
const storages : ConfiguredStorage [ ] = [ ] ;
for ( const configPath of orderedConfigPaths ) {
const resolved = path . resolve ( configPath ) ;
if ( seen . has ( resolved ) || ! existsSync ( resolved ) ) continue ;
seen . add ( resolved ) ;
storages . push ( openConfiguredStorage ( resolved ) ) ;
}
return storages ;
}
2026-03-20 15:02:24 -05:00
async function openConfiguredDb ( configPath : string ) : Promise < OpenDbHandle > {
const config = readConfig ( configPath ) ;
if ( ! config ) {
throw new Error ( ` Config not found at ${ configPath } . ` ) ;
}
const envEntries = readPaperclipEnvEntries ( resolvePaperclipEnvFile ( configPath ) ) ;
let embeddedHandle : EmbeddedPostgresHandle | null = null ;
try {
if ( config . database . mode === "embedded-postgres" ) {
embeddedHandle = await ensureEmbeddedPostgres (
config . database . embeddedPostgresDataDir ,
config . database . embeddedPostgresPort ,
) ;
}
const connectionString = resolveSourceConnectionString ( config , envEntries , embeddedHandle ? . port ) ;
2026-03-20 17:23:45 -05:00
const migrationState = await inspectMigrations ( connectionString ) ;
if ( migrationState . status !== "upToDate" ) {
const pending =
migrationState . reason === "pending-migrations"
? ` Pending migrations: ${ migrationState . pendingMigrations . join ( ", " ) } . `
: "" ;
throw new Error (
` Database for ${ configPath } is not up to date. ${ pending } Run \` pnpm db:migrate \` (or start Paperclip once) before using worktree merge history. ` ,
) ;
}
2026-03-20 15:02:24 -05:00
const db = createDb ( connectionString ) as ClosableDb ;
return {
db ,
stop : async ( ) = > {
await closeDb ( db ) ;
if ( embeddedHandle ? . startedByThisProcess ) {
await embeddedHandle . stop ( ) ;
}
} ,
} ;
} catch ( error ) {
if ( embeddedHandle ? . startedByThisProcess ) {
await embeddedHandle . stop ( ) . catch ( ( ) = > undefined ) ;
}
throw error ;
}
}
async function resolveMergeCompany ( input : {
sourceDb : ClosableDb ;
targetDb : ClosableDb ;
selector? : string ;
} ) : Promise < ResolvedMergeCompany > {
const [ sourceCompanies , targetCompanies ] = await Promise . all ( [
input . sourceDb
. select ( {
id : companies.id ,
name : companies.name ,
issuePrefix : companies.issuePrefix ,
} )
. from ( companies ) ,
input . targetDb
. select ( {
id : companies.id ,
name : companies.name ,
issuePrefix : companies.issuePrefix ,
} )
. from ( companies ) ,
] ) ;
const targetById = new Map ( targetCompanies . map ( ( company ) = > [ company . id , company ] ) ) ;
const shared = sourceCompanies . filter ( ( company ) = > targetById . has ( company . id ) ) ;
const selector = nonEmpty ( input . selector ) ;
if ( selector ) {
const matched = shared . find (
( company ) = > company . id === selector || company . issuePrefix . toLowerCase ( ) === selector . toLowerCase ( ) ,
) ;
if ( ! matched ) {
throw new Error ( ` Could not resolve company " ${ selector } " in both source and target databases. ` ) ;
}
return matched ;
}
if ( shared . length === 1 ) {
return shared [ 0 ] ;
}
if ( shared . length === 0 ) {
throw new Error ( "Source and target databases do not share a company id. Pass --company explicitly once both sides match." ) ;
}
const options = shared
. map ( ( company ) = > ` ${ company . issuePrefix } ( ${ company . name } ) ` )
. join ( ", " ) ;
throw new Error ( ` Multiple shared companies found. Re-run with --company <id-or-prefix>. Options: ${ options } ` ) ;
}
function renderMergePlan ( plan : Awaited < ReturnType < typeof collectMergePlan > > [ "plan" ] , extras : {
sourcePath : string ;
2026-03-20 15:39:02 -05:00
targetPath : string ;
2026-03-20 15:02:24 -05:00
unsupportedRunCount : number ;
} ) : string {
2026-03-20 15:44:22 -05:00
const terminalWidth = Math . max ( 60 , process . stdout . columns ? ? 100 ) ;
const oneLine = ( value : string ) = > value . replace ( /\s+/g , " " ) . trim ( ) ;
const truncateToWidth = ( value : string , maxWidth : number ) = > {
if ( maxWidth <= 1 ) return "" ;
if ( value . length <= maxWidth ) return value ;
return ` ${ value . slice ( 0 , Math . max ( 0 , maxWidth - 1 ) ) . trimEnd ( ) } … ` ;
} ;
2026-03-20 15:02:24 -05:00
const lines = [
` Mode: preview ` ,
` Source: ${ extras . sourcePath } ` ,
2026-03-20 15:39:02 -05:00
` Target: ${ extras . targetPath } ` ,
2026-03-20 15:02:24 -05:00
` Company: ${ plan . companyName } ( ${ plan . issuePrefix } ) ` ,
"" ,
2026-03-21 17:09:46 -05:00
"Projects" ,
` - import: ${ plan . counts . projectsToImport } ` ,
"" ,
2026-03-20 15:02:24 -05:00
"Issues" ,
` - insert: ${ plan . counts . issuesToInsert } ` ,
` - already present: ${ plan . counts . issuesExisting } ` ,
` - shared/imported issues with drift: ${ plan . counts . issueDrift } ` ,
] ;
2026-03-21 17:09:46 -05:00
if ( plan . projectImports . length > 0 ) {
lines . push ( "" ) ;
lines . push ( "Planned project imports" ) ;
for ( const project of plan . projectImports ) {
lines . push (
` - ${ project . source . name } ( ${ project . workspaces . length } workspace ${ project . workspaces . length === 1 ? "" : "s" } ) ` ,
) ;
}
}
2026-03-20 15:02:24 -05:00
const issueInserts = plan . issuePlans . filter ( ( item ) : item is PlannedIssueInsert = > item . action === "insert" ) ;
if ( issueInserts . length > 0 ) {
lines . push ( "" ) ;
lines . push ( "Planned issue imports" ) ;
for ( const issue of issueInserts ) {
2026-03-20 15:13:35 -05:00
const projectNote =
2026-03-21 17:09:46 -05:00
( issue . projectResolution === "mapped" || issue . projectResolution === "imported" )
&& issue . mappedProjectName
? ` project-> ${ issue . projectResolution === "imported" ? "import:" : "" } ${ issue . mappedProjectName } `
2026-03-20 15:13:35 -05:00
: "" ;
2026-03-20 15:02:24 -05:00
const adjustments = issue . adjustments . length > 0 ? ` [ ${ issue . adjustments . join ( ", " ) } ] ` : "" ;
2026-03-20 15:44:22 -05:00
const prefix = ` - ${ issue . source . identifier ? ? issue . source . id } -> ${ issue . previewIdentifier } ( ${ issue . targetStatus } ${ projectNote } ) ` ;
const title = oneLine ( issue . source . title ) ;
const suffix = ` ${ adjustments } ${ title ? ` ${ title } ` : "" } ` ;
2026-03-20 15:02:24 -05:00
lines . push (
2026-03-20 15:44:22 -05:00
` ${ prefix } ${ truncateToWidth ( suffix , Math . max ( 8 , terminalWidth - prefix . length ) ) } ` ,
2026-03-20 15:02:24 -05:00
) ;
}
}
if ( plan . scopes . includes ( "comments" ) ) {
lines . push ( "" ) ;
lines . push ( "Comments" ) ;
lines . push ( ` - insert: ${ plan . counts . commentsToInsert } ` ) ;
lines . push ( ` - already present: ${ plan . counts . commentsExisting } ` ) ;
lines . push ( ` - skipped (missing parent): ${ plan . counts . commentsMissingParent } ` ) ;
}
2026-03-20 15:59:41 -05:00
lines . push ( "" ) ;
lines . push ( "Documents" ) ;
lines . push ( ` - insert: ${ plan . counts . documentsToInsert } ` ) ;
lines . push ( ` - merge existing: ${ plan . counts . documentsToMerge } ` ) ;
lines . push ( ` - already present: ${ plan . counts . documentsExisting } ` ) ;
lines . push ( ` - skipped (conflicting key): ${ plan . counts . documentsConflictingKey } ` ) ;
lines . push ( ` - skipped (missing parent): ${ plan . counts . documentsMissingParent } ` ) ;
lines . push ( ` - revisions insert: ${ plan . counts . documentRevisionsToInsert } ` ) ;
lines . push ( "" ) ;
lines . push ( "Attachments" ) ;
lines . push ( ` - insert: ${ plan . counts . attachmentsToInsert } ` ) ;
lines . push ( ` - already present: ${ plan . counts . attachmentsExisting } ` ) ;
lines . push ( ` - skipped (missing parent): ${ plan . counts . attachmentsMissingParent } ` ) ;
2026-03-20 15:02:24 -05:00
lines . push ( "" ) ;
lines . push ( "Adjustments" ) ;
lines . push ( ` - cleared assignee agents: ${ plan . adjustments . clear_assignee_agent } ` ) ;
lines . push ( ` - cleared projects: ${ plan . adjustments . clear_project } ` ) ;
lines . push ( ` - cleared project workspaces: ${ plan . adjustments . clear_project_workspace } ` ) ;
lines . push ( ` - cleared goals: ${ plan . adjustments . clear_goal } ` ) ;
lines . push ( ` - cleared comment author agents: ${ plan . adjustments . clear_author_agent } ` ) ;
2026-03-20 15:59:41 -05:00
lines . push ( ` - cleared document agents: ${ plan . adjustments . clear_document_agent } ` ) ;
lines . push ( ` - cleared document revision agents: ${ plan . adjustments . clear_document_revision_agent } ` ) ;
lines . push ( ` - cleared attachment author agents: ${ plan . adjustments . clear_attachment_agent } ` ) ;
2026-03-20 15:02:24 -05:00
lines . push ( ` - coerced in_progress to todo: ${ plan . adjustments . coerce_in_progress_to_todo } ` ) ;
lines . push ( "" ) ;
lines . push ( "Not imported in this phase" ) ;
lines . push ( ` - heartbeat runs: ${ extras . unsupportedRunCount } ` ) ;
lines . push ( "" ) ;
lines . push ( "Identifiers shown above are provisional preview values. `--apply` reserves fresh issue numbers at write time." ) ;
return lines . join ( "\n" ) ;
}
2026-04-07 07:48:22 -05:00
function resolveRunningEmbeddedPostgresPid ( config : PaperclipConfig ) : number | null {
if ( config . database . mode !== "embedded-postgres" ) {
return null ;
}
return readRunningPostmasterPid ( path . resolve ( config . database . embeddedPostgresDataDir , "postmaster.pid" ) ) ;
}
2026-03-20 15:02:24 -05:00
async function collectMergePlan ( input : {
sourceDb : ClosableDb ;
targetDb : ClosableDb ;
company : ResolvedMergeCompany ;
scopes : ReturnType < typeof parseWorktreeMergeScopes > ;
2026-03-21 17:09:46 -05:00
importProjectIds? : Iterable < string > ;
2026-03-20 15:13:35 -05:00
projectIdOverrides? : Record < string , string | null | undefined > ;
2026-03-20 15:02:24 -05:00
} ) {
const companyId = input . company . id ;
2026-03-20 15:59:41 -05:00
const [
targetCompanyRow ,
sourceIssuesRows ,
targetIssuesRows ,
sourceCommentsRows ,
targetCommentsRows ,
sourceIssueDocumentsRows ,
targetIssueDocumentsRows ,
sourceDocumentRevisionRows ,
targetDocumentRevisionRows ,
sourceAttachmentRows ,
targetAttachmentRows ,
sourceProjectsRows ,
2026-03-21 17:09:46 -05:00
sourceProjectWorkspaceRows ,
2026-03-20 15:59:41 -05:00
targetProjectsRows ,
targetAgentsRows ,
targetProjectWorkspaceRows ,
targetGoalsRows ,
runCountRows ,
] = await Promise . all ( [
2026-03-20 15:02:24 -05:00
input . targetDb
. select ( {
issueCounter : companies.issueCounter ,
} )
. from ( companies )
. where ( eq ( companies . id , companyId ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ,
input . sourceDb
. select ( )
. from ( issues )
. where ( eq ( issues . companyId , companyId ) ) ,
input . targetDb
. select ( )
. from ( issues )
. where ( eq ( issues . companyId , companyId ) ) ,
input . scopes . includes ( "comments" )
? input . sourceDb
. select ( )
. from ( issueComments )
. where ( eq ( issueComments . companyId , companyId ) )
: Promise . resolve ( [ ] ) ,
2026-03-20 15:59:41 -05:00
input . targetDb
. select ( )
. from ( issueComments )
. where ( eq ( issueComments . companyId , companyId ) ) ,
input . sourceDb
. select ( {
id : issueDocuments.id ,
companyId : issueDocuments.companyId ,
issueId : issueDocuments.issueId ,
documentId : issueDocuments.documentId ,
key : issueDocuments.key ,
linkCreatedAt : issueDocuments.createdAt ,
linkUpdatedAt : issueDocuments.updatedAt ,
title : documents.title ,
format : documents.format ,
latestBody : documents.latestBody ,
latestRevisionId : documents.latestRevisionId ,
latestRevisionNumber : documents.latestRevisionNumber ,
createdByAgentId : documents.createdByAgentId ,
createdByUserId : documents.createdByUserId ,
updatedByAgentId : documents.updatedByAgentId ,
updatedByUserId : documents.updatedByUserId ,
documentCreatedAt : documents.createdAt ,
documentUpdatedAt : documents.updatedAt ,
} )
. from ( issueDocuments )
. innerJoin ( documents , eq ( issueDocuments . documentId , documents . id ) )
. innerJoin ( issues , eq ( issueDocuments . issueId , issues . id ) )
. where ( eq ( issues . companyId , companyId ) ) ,
input . targetDb
. select ( {
id : issueDocuments.id ,
companyId : issueDocuments.companyId ,
issueId : issueDocuments.issueId ,
documentId : issueDocuments.documentId ,
key : issueDocuments.key ,
linkCreatedAt : issueDocuments.createdAt ,
linkUpdatedAt : issueDocuments.updatedAt ,
title : documents.title ,
format : documents.format ,
latestBody : documents.latestBody ,
latestRevisionId : documents.latestRevisionId ,
latestRevisionNumber : documents.latestRevisionNumber ,
createdByAgentId : documents.createdByAgentId ,
createdByUserId : documents.createdByUserId ,
updatedByAgentId : documents.updatedByAgentId ,
updatedByUserId : documents.updatedByUserId ,
documentCreatedAt : documents.createdAt ,
documentUpdatedAt : documents.updatedAt ,
} )
. from ( issueDocuments )
. innerJoin ( documents , eq ( issueDocuments . documentId , documents . id ) )
. innerJoin ( issues , eq ( issueDocuments . issueId , issues . id ) )
. where ( eq ( issues . companyId , companyId ) ) ,
input . sourceDb
. select ( {
id : documentRevisions.id ,
companyId : documentRevisions.companyId ,
documentId : documentRevisions.documentId ,
revisionNumber : documentRevisions.revisionNumber ,
body : documentRevisions.body ,
changeSummary : documentRevisions.changeSummary ,
createdByAgentId : documentRevisions.createdByAgentId ,
createdByUserId : documentRevisions.createdByUserId ,
createdAt : documentRevisions.createdAt ,
} )
. from ( documentRevisions )
. innerJoin ( issueDocuments , eq ( documentRevisions . documentId , issueDocuments . documentId ) )
. innerJoin ( issues , eq ( issueDocuments . issueId , issues . id ) )
. where ( eq ( issues . companyId , companyId ) ) ,
input . targetDb
. select ( {
id : documentRevisions.id ,
companyId : documentRevisions.companyId ,
documentId : documentRevisions.documentId ,
revisionNumber : documentRevisions.revisionNumber ,
body : documentRevisions.body ,
changeSummary : documentRevisions.changeSummary ,
createdByAgentId : documentRevisions.createdByAgentId ,
createdByUserId : documentRevisions.createdByUserId ,
createdAt : documentRevisions.createdAt ,
} )
. from ( documentRevisions )
. innerJoin ( issueDocuments , eq ( documentRevisions . documentId , issueDocuments . documentId ) )
. innerJoin ( issues , eq ( issueDocuments . issueId , issues . id ) )
. where ( eq ( issues . companyId , companyId ) ) ,
input . sourceDb
. select ( {
id : issueAttachments.id ,
companyId : issueAttachments.companyId ,
issueId : issueAttachments.issueId ,
issueCommentId : issueAttachments.issueCommentId ,
assetId : issueAttachments.assetId ,
provider : assets.provider ,
objectKey : assets.objectKey ,
contentType : assets.contentType ,
byteSize : assets.byteSize ,
sha256 : assets.sha256 ,
originalFilename : assets.originalFilename ,
createdByAgentId : assets.createdByAgentId ,
createdByUserId : assets.createdByUserId ,
assetCreatedAt : assets.createdAt ,
assetUpdatedAt : assets.updatedAt ,
attachmentCreatedAt : issueAttachments.createdAt ,
attachmentUpdatedAt : issueAttachments.updatedAt ,
} )
. from ( issueAttachments )
. innerJoin ( assets , eq ( issueAttachments . assetId , assets . id ) )
. innerJoin ( issues , eq ( issueAttachments . issueId , issues . id ) )
. where ( eq ( issues . companyId , companyId ) ) ,
input . targetDb
. select ( {
id : issueAttachments.id ,
companyId : issueAttachments.companyId ,
issueId : issueAttachments.issueId ,
issueCommentId : issueAttachments.issueCommentId ,
assetId : issueAttachments.assetId ,
provider : assets.provider ,
objectKey : assets.objectKey ,
contentType : assets.contentType ,
byteSize : assets.byteSize ,
sha256 : assets.sha256 ,
originalFilename : assets.originalFilename ,
createdByAgentId : assets.createdByAgentId ,
createdByUserId : assets.createdByUserId ,
assetCreatedAt : assets.createdAt ,
assetUpdatedAt : assets.updatedAt ,
attachmentCreatedAt : issueAttachments.createdAt ,
attachmentUpdatedAt : issueAttachments.updatedAt ,
} )
. from ( issueAttachments )
. innerJoin ( assets , eq ( issueAttachments . assetId , assets . id ) )
. innerJoin ( issues , eq ( issueAttachments . issueId , issues . id ) )
. where ( eq ( issues . companyId , companyId ) ) ,
2026-03-20 15:13:35 -05:00
input . sourceDb
2026-03-20 15:02:24 -05:00
. select ( )
2026-03-20 15:13:35 -05:00
. from ( projects )
. where ( eq ( projects . companyId , companyId ) ) ,
2026-03-21 17:09:46 -05:00
input . sourceDb
. select ( )
. from ( projectWorkspaces )
. where ( eq ( projectWorkspaces . companyId , companyId ) ) ,
2026-03-20 15:02:24 -05:00
input . targetDb
. select ( )
. from ( projects )
. where ( eq ( projects . companyId , companyId ) ) ,
2026-03-20 15:13:35 -05:00
input . targetDb
. select ( )
. from ( agents )
. where ( eq ( agents . companyId , companyId ) ) ,
2026-03-20 15:02:24 -05:00
input . targetDb
. select ( )
. from ( projectWorkspaces )
. where ( eq ( projectWorkspaces . companyId , companyId ) ) ,
input . targetDb
. select ( )
. from ( goals )
. where ( eq ( goals . companyId , companyId ) ) ,
input . sourceDb
. select ( { count : sql < number > ` count(*)::int ` } )
. from ( heartbeatRuns )
. where ( eq ( heartbeatRuns . companyId , companyId ) ) ,
] ) ;
if ( ! targetCompanyRow ) {
throw new Error ( ` Target company ${ companyId } was not found. ` ) ;
}
const plan = buildWorktreeMergePlan ( {
companyId ,
companyName : input.company.name ,
issuePrefix : input.company.issuePrefix ,
previewIssueCounterStart : targetCompanyRow.issueCounter ,
scopes : input.scopes ,
sourceIssues : sourceIssuesRows ,
targetIssues : targetIssuesRows ,
sourceComments : sourceCommentsRows ,
targetComments : targetCommentsRows ,
2026-03-21 17:09:46 -05:00
sourceProjects : sourceProjectsRows ,
sourceProjectWorkspaces : sourceProjectWorkspaceRows ,
2026-03-20 15:59:41 -05:00
sourceDocuments : sourceIssueDocumentsRows as IssueDocumentRow [ ] ,
targetDocuments : targetIssueDocumentsRows as IssueDocumentRow [ ] ,
sourceDocumentRevisions : sourceDocumentRevisionRows as DocumentRevisionRow [ ] ,
targetDocumentRevisions : targetDocumentRevisionRows as DocumentRevisionRow [ ] ,
sourceAttachments : sourceAttachmentRows as IssueAttachmentRow [ ] ,
targetAttachments : targetAttachmentRows as IssueAttachmentRow [ ] ,
2026-03-20 15:02:24 -05:00
targetAgents : targetAgentsRows ,
targetProjects : targetProjectsRows ,
targetProjectWorkspaces : targetProjectWorkspaceRows ,
targetGoals : targetGoalsRows ,
2026-03-21 17:09:46 -05:00
importProjectIds : input.importProjectIds ,
2026-03-20 15:13:35 -05:00
projectIdOverrides : input.projectIdOverrides ,
2026-03-20 15:02:24 -05:00
} ) ;
return {
plan ,
2026-03-20 15:13:35 -05:00
sourceProjects : sourceProjectsRows ,
targetProjects : targetProjectsRows ,
2026-03-20 15:02:24 -05:00
unsupportedRunCount : runCountRows [ 0 ] ? . count ? ? 0 ,
} ;
}
2026-03-21 17:09:46 -05:00
type ProjectMappingSelections = {
importProjectIds : string [ ] ;
projectIdOverrides : Record < string , string | null > ;
} ;
2026-03-20 15:13:35 -05:00
async function promptForProjectMappings ( input : {
plan : Awaited < ReturnType < typeof collectMergePlan > > [ "plan" ] ;
sourceProjects : Awaited < ReturnType < typeof collectMergePlan > > [ "sourceProjects" ] ;
targetProjects : Awaited < ReturnType < typeof collectMergePlan > > [ "targetProjects" ] ;
2026-03-21 17:09:46 -05:00
} ) : Promise < ProjectMappingSelections > {
2026-03-20 15:13:35 -05:00
const missingProjectIds = [
. . . new Set (
input . plan . issuePlans
. filter ( ( plan ) : plan is PlannedIssueInsert = > plan . action === "insert" )
. filter ( ( plan ) = > ! ! plan . source . projectId && plan . projectResolution === "cleared" )
. map ( ( plan ) = > plan . source . projectId as string ) ,
) ,
] ;
2026-03-21 17:09:46 -05:00
if ( missingProjectIds . length === 0 ) {
return {
importProjectIds : [ ] ,
projectIdOverrides : { } ,
} ;
2026-03-20 15:13:35 -05:00
}
const sourceProjectsById = new Map ( input . sourceProjects . map ( ( project ) = > [ project . id , project ] ) ) ;
const targetChoices = [ . . . input . targetProjects ]
. sort ( ( left , right ) = > left . name . localeCompare ( right . name ) )
. map ( ( project ) = > ( {
value : project.id ,
label : project.name ,
hint : project.status ,
} ) ) ;
const mappings : Record < string , string | null > = { } ;
2026-03-21 17:09:46 -05:00
const importProjectIds = new Set < string > ( ) ;
2026-03-20 15:13:35 -05:00
for ( const sourceProjectId of missingProjectIds ) {
const sourceProject = sourceProjectsById . get ( sourceProjectId ) ;
if ( ! sourceProject ) continue ;
const nameMatch = input . targetProjects . find (
( project ) = > project . name . trim ( ) . toLowerCase ( ) === sourceProject . name . trim ( ) . toLowerCase ( ) ,
) ;
2026-03-21 17:09:46 -05:00
const importSelectionValue = ` __import__: ${ sourceProjectId } ` ;
2026-03-20 15:13:35 -05:00
const selection = await p . select < string | null > ( {
message : ` Project " ${ sourceProject . name } " is missing in target. How should ${ input . plan . issuePrefix } imports handle it? ` ,
options : [
2026-03-21 17:09:46 -05:00
{
value : importSelectionValue ,
label : ` Import ${ sourceProject . name } ` ,
hint : "Create the project and copy its workspace settings" ,
} ,
2026-03-20 15:13:35 -05:00
. . . ( nameMatch
? [ {
value : nameMatch.id ,
label : ` Map to ${ nameMatch . name } ` ,
hint : "Recommended: exact name match" ,
} ]
: [ ] ) ,
{
value : null ,
label : "Leave unset" ,
hint : "Keep imported issues without a project" ,
} ,
. . . targetChoices . filter ( ( choice ) = > choice . value !== nameMatch ? . id ) ,
] ,
initialValue : nameMatch?.id ? ? null ,
} ) ;
if ( p . isCancel ( selection ) ) {
throw new Error ( "Project mapping cancelled." ) ;
}
2026-03-21 17:09:46 -05:00
if ( selection === importSelectionValue ) {
importProjectIds . add ( sourceProjectId ) ;
continue ;
}
2026-03-20 15:13:35 -05:00
mappings [ sourceProjectId ] = selection ;
}
2026-03-21 17:09:46 -05:00
return {
importProjectIds : [ . . . importProjectIds ] ,
projectIdOverrides : mappings ,
} ;
2026-03-20 15:13:35 -05:00
}
2026-03-20 15:17:51 -05:00
export async function worktreeListCommand ( opts : WorktreeListOptions ) : Promise < void > {
const choices = toMergeSourceChoices ( process . cwd ( ) ) ;
if ( opts . json ) {
console . log ( JSON . stringify ( choices , null , 2 ) ) ;
return ;
}
for ( const choice of choices ) {
const flags = [
choice . isCurrent ? "current" : null ,
choice . hasPaperclipConfig ? "paperclip" : "no-paperclip-config" ,
] . filter ( ( value ) : value is string = > value !== null ) ;
p . log . message ( ` ${ choice . branchLabel } ${ choice . worktree } [ ${ flags . join ( ", " ) } ] ` ) ;
}
}
2026-03-20 15:39:02 -05:00
function resolveEndpointFromChoice ( choice : MergeSourceChoice ) : ResolvedWorktreeEndpoint {
if ( choice . isCurrent ) {
return resolveCurrentEndpoint ( ) ;
}
return {
rootPath : choice.worktree ,
configPath : path.resolve ( choice . worktree , ".paperclip" , "config.json" ) ,
label : choice.branchLabel ,
isCurrent : false ,
} ;
}
2026-03-20 15:17:51 -05:00
2026-03-20 15:39:02 -05:00
function resolveWorktreeEndpointFromSelector (
selector : string ,
opts ? : { allowCurrent? : boolean } ,
) : ResolvedWorktreeEndpoint {
const trimmed = selector . trim ( ) ;
const allowCurrent = opts ? . allowCurrent !== false ;
if ( trimmed . length === 0 ) {
throw new Error ( "Worktree selector cannot be empty." ) ;
}
2026-03-20 15:17:51 -05:00
2026-03-20 15:39:02 -05:00
const currentEndpoint = resolveCurrentEndpoint ( ) ;
if ( allowCurrent && trimmed === "current" ) {
return currentEndpoint ;
}
const choices = toMergeSourceChoices ( process . cwd ( ) ) ;
const directPath = path . resolve ( trimmed ) ;
if ( existsSync ( directPath ) ) {
if ( allowCurrent && directPath === currentEndpoint . rootPath ) {
return currentEndpoint ;
}
const configPath = path . resolve ( directPath , ".paperclip" , "config.json" ) ;
if ( ! existsSync ( configPath ) ) {
throw new Error ( ` Resolved worktree path ${ directPath } does not contain .paperclip/config.json. ` ) ;
2026-03-20 15:17:51 -05:00
}
2026-03-20 15:39:02 -05:00
return {
rootPath : directPath ,
configPath ,
label : path.basename ( directPath ) ,
isCurrent : false ,
} ;
}
2026-03-20 15:17:51 -05:00
2026-03-20 15:39:02 -05:00
const matched = choices . find ( ( choice ) = >
( allowCurrent || ! choice . isCurrent )
&& ( choice . worktree === directPath
|| path . basename ( choice . worktree ) === trimmed
|| choice . branchLabel === trimmed ) ,
) ;
if ( ! matched ) {
2026-03-20 15:17:51 -05:00
throw new Error (
2026-03-20 15:39:02 -05:00
` Could not resolve worktree " ${ selector } ". Use a path, a listed worktree directory name, branch name, or "current". ` ,
2026-03-20 15:17:51 -05:00
) ;
}
2026-03-20 15:39:02 -05:00
if ( ! matched . hasPaperclipConfig && ! matched . isCurrent ) {
throw new Error ( ` Resolved worktree " ${ selector } " does not look like a Paperclip worktree. ` ) ;
2026-03-20 15:17:51 -05:00
}
2026-03-20 15:39:02 -05:00
return resolveEndpointFromChoice ( matched ) ;
}
2026-03-20 15:17:51 -05:00
2026-03-20 15:39:02 -05:00
async function promptForSourceEndpoint ( excludeWorktreePath? : string ) : Promise < ResolvedWorktreeEndpoint > {
const excluded = excludeWorktreePath ? path . resolve ( excludeWorktreePath ) : null ;
const currentEndpoint = resolveCurrentEndpoint ( ) ;
const choices = toMergeSourceChoices ( process . cwd ( ) )
. filter ( ( choice ) = > choice . hasPaperclipConfig || choice . isCurrent )
. filter ( ( choice ) = > path . resolve ( choice . worktree ) !== excluded )
. map ( ( choice ) = > ( {
value : choice.isCurrent ? "__current__" : choice . worktree ,
label : choice.branchLabel ,
hint : ` ${ choice . worktree } ${ choice . isCurrent ? " (current)" : "" } ` ,
} ) ) ;
if ( choices . length === 0 ) {
throw new Error ( "No Paperclip worktrees were found. Run `paperclipai worktree:list` to inspect the repo worktrees." ) ;
}
2026-03-20 15:17:51 -05:00
const selection = await p . select < string > ( {
message : "Choose the source worktree to import from" ,
2026-03-20 15:39:02 -05:00
options : choices ,
2026-03-20 15:17:51 -05:00
} ) ;
if ( p . isCancel ( selection ) ) {
throw new Error ( "Source worktree selection cancelled." ) ;
}
2026-03-20 15:39:02 -05:00
if ( selection === "__current__" ) {
return currentEndpoint ;
}
return resolveWorktreeEndpointFromSelector ( selection , { allowCurrent : true } ) ;
2026-03-20 15:17:51 -05:00
}
2026-03-20 15:02:24 -05:00
async function applyMergePlan ( input : {
2026-03-20 16:12:10 -05:00
sourceStorages : ConfiguredStorage [ ] ;
2026-03-20 15:59:41 -05:00
targetStorage : ConfiguredStorage ;
2026-03-20 15:02:24 -05:00
targetDb : ClosableDb ;
company : ResolvedMergeCompany ;
plan : Awaited < ReturnType < typeof collectMergePlan > > [ "plan" ] ;
} ) {
const companyId = input . company . id ;
return await input . targetDb . transaction ( async ( tx ) = > {
2026-03-21 17:09:46 -05:00
const importedProjectIds = input . plan . projectImports . map ( ( project ) = > project . source . id ) ;
const existingImportedProjectIds = importedProjectIds . length > 0
? new Set (
( await tx
. select ( { id : projects.id } )
. from ( projects )
. where ( inArray ( projects . id , importedProjectIds ) ) )
. map ( ( row ) = > row . id ) ,
)
: new Set < string > ( ) ;
const projectImports = input . plan . projectImports . filter ( ( project ) = > ! existingImportedProjectIds . has ( project . source . id ) ) ;
const importedWorkspaceIds = projectImports . flatMap ( ( project ) = > project . workspaces . map ( ( workspace ) = > workspace . id ) ) ;
const existingImportedWorkspaceIds = importedWorkspaceIds . length > 0
? new Set (
( await tx
. select ( { id : projectWorkspaces.id } )
. from ( projectWorkspaces )
. where ( inArray ( projectWorkspaces . id , importedWorkspaceIds ) ) )
. map ( ( row ) = > row . id ) ,
)
: new Set < string > ( ) ;
let insertedProjects = 0 ;
let insertedProjectWorkspaces = 0 ;
for ( const project of projectImports ) {
await tx . insert ( projects ) . values ( {
id : project.source.id ,
companyId ,
goalId : project.targetGoalId ,
name : project.source.name ,
description : project.source.description ,
status : project.source.status ,
leadAgentId : project.targetLeadAgentId ,
targetDate : project.source.targetDate ,
color : project.source.color ,
pauseReason : project.source.pauseReason ,
pausedAt : project.source.pausedAt ,
executionWorkspacePolicy : project.source.executionWorkspacePolicy ,
archivedAt : project.source.archivedAt ,
createdAt : project.source.createdAt ,
updatedAt : project.source.updatedAt ,
} ) ;
insertedProjects += 1 ;
for ( const workspace of project . workspaces ) {
if ( existingImportedWorkspaceIds . has ( workspace . id ) ) continue ;
await tx . insert ( projectWorkspaces ) . values ( {
id : workspace.id ,
companyId ,
projectId : project.source.id ,
name : workspace.name ,
sourceType : workspace.sourceType ,
cwd : workspace.cwd ,
repoUrl : workspace.repoUrl ,
repoRef : workspace.repoRef ,
defaultRef : workspace.defaultRef ,
visibility : workspace.visibility ,
setupCommand : workspace.setupCommand ,
cleanupCommand : workspace.cleanupCommand ,
remoteProvider : workspace.remoteProvider ,
remoteWorkspaceRef : workspace.remoteWorkspaceRef ,
sharedWorkspaceKey : workspace.sharedWorkspaceKey ,
metadata : workspace.metadata ,
isPrimary : workspace.isPrimary ,
createdAt : workspace.createdAt ,
updatedAt : workspace.updatedAt ,
} ) ;
insertedProjectWorkspaces += 1 ;
}
}
2026-03-20 15:02:24 -05:00
const issueCandidates = input . plan . issuePlans . filter (
( plan ) : plan is PlannedIssueInsert = > plan . action === "insert" ,
) ;
const issueCandidateIds = issueCandidates . map ( ( issue ) = > issue . source . id ) ;
const existingIssueIds = issueCandidateIds . length > 0
? new Set (
( await tx
. select ( { id : issues.id } )
. from ( issues )
. where ( inArray ( issues . id , issueCandidateIds ) ) )
. map ( ( row ) = > row . id ) ,
)
: new Set < string > ( ) ;
const issueInserts = issueCandidates . filter ( ( issue ) = > ! existingIssueIds . has ( issue . source . id ) ) ;
let nextIssueNumber = 0 ;
if ( issueInserts . length > 0 ) {
const [ companyRow ] = await tx
. update ( companies )
. set ( { issueCounter : sql ` ${ companies . issueCounter } + ${ issueInserts . length } ` } )
. where ( eq ( companies . id , companyId ) )
. returning ( { issueCounter : companies.issueCounter } ) ;
nextIssueNumber = companyRow . issueCounter - issueInserts . length + 1 ;
}
const insertedIssueIdentifiers = new Map < string , string > ( ) ;
2026-03-20 15:59:41 -05:00
let insertedIssues = 0 ;
2026-03-20 15:02:24 -05:00
for ( const issue of issueInserts ) {
const issueNumber = nextIssueNumber ;
nextIssueNumber += 1 ;
const identifier = ` ${ input . company . issuePrefix } - ${ issueNumber } ` ;
insertedIssueIdentifiers . set ( issue . source . id , identifier ) ;
await tx . insert ( issues ) . values ( {
id : issue.source.id ,
companyId ,
projectId : issue.targetProjectId ,
projectWorkspaceId : issue.targetProjectWorkspaceId ,
goalId : issue.targetGoalId ,
parentId : issue.source.parentId ,
title : issue.source.title ,
description : issue.source.description ,
status : issue.targetStatus ,
priority : issue.source.priority ,
assigneeAgentId : issue.targetAssigneeAgentId ,
assigneeUserId : issue.source.assigneeUserId ,
checkoutRunId : null ,
executionRunId : null ,
executionAgentNameKey : null ,
executionLockedAt : null ,
createdByAgentId : issue.targetCreatedByAgentId ,
createdByUserId : issue.source.createdByUserId ,
issueNumber ,
identifier ,
requestDepth : issue.source.requestDepth ,
billingCode : issue.source.billingCode ,
assigneeAdapterOverrides : issue.targetAssigneeAgentId ? issue.source.assigneeAdapterOverrides : null ,
executionWorkspaceId : null ,
executionWorkspacePreference : null ,
executionWorkspaceSettings : null ,
startedAt : issue.source.startedAt ,
completedAt : issue.source.completedAt ,
cancelledAt : issue.source.cancelledAt ,
hiddenAt : issue.source.hiddenAt ,
createdAt : issue.source.createdAt ,
updatedAt : issue.source.updatedAt ,
} ) ;
2026-03-20 15:59:41 -05:00
insertedIssues += 1 ;
2026-03-20 15:02:24 -05:00
}
const commentCandidates = input . plan . commentPlans . filter (
( plan ) : plan is PlannedCommentInsert = > plan . action === "insert" ,
) ;
const commentCandidateIds = commentCandidates . map ( ( comment ) = > comment . source . id ) ;
const existingCommentIds = commentCandidateIds . length > 0
? new Set (
( await tx
. select ( { id : issueComments.id } )
. from ( issueComments )
. where ( inArray ( issueComments . id , commentCandidateIds ) ) )
. map ( ( row ) = > row . id ) ,
)
: new Set < string > ( ) ;
2026-03-20 15:59:41 -05:00
let insertedComments = 0 ;
2026-03-20 15:02:24 -05:00
for ( const comment of commentCandidates ) {
if ( existingCommentIds . has ( comment . source . id ) ) continue ;
const parentExists = await tx
. select ( { id : issues.id } )
. from ( issues )
. where ( and ( eq ( issues . id , comment . source . issueId ) , eq ( issues . companyId , companyId ) ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
if ( ! parentExists ) continue ;
await tx . insert ( issueComments ) . values ( {
id : comment.source.id ,
companyId ,
issueId : comment.source.issueId ,
authorAgentId : comment.targetAuthorAgentId ,
authorUserId : comment.source.authorUserId ,
body : comment.source.body ,
createdAt : comment.source.createdAt ,
updatedAt : comment.source.updatedAt ,
} ) ;
2026-03-20 15:59:41 -05:00
insertedComments += 1 ;
}
const documentCandidates = input . plan . documentPlans . filter (
( plan ) : plan is PlannedIssueDocumentInsert | PlannedIssueDocumentMerge = >
plan . action === "insert" || plan . action === "merge_existing" ,
) ;
let insertedDocuments = 0 ;
let mergedDocuments = 0 ;
let insertedDocumentRevisions = 0 ;
for ( const documentPlan of documentCandidates ) {
const parentExists = await tx
. select ( { id : issues.id } )
. from ( issues )
. where ( and ( eq ( issues . id , documentPlan . source . issueId ) , eq ( issues . companyId , companyId ) ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
if ( ! parentExists ) continue ;
const conflictingKeyDocument = await tx
. select ( { documentId : issueDocuments.documentId } )
. from ( issueDocuments )
. where ( and ( eq ( issueDocuments . issueId , documentPlan . source . issueId ) , eq ( issueDocuments . key , documentPlan . source . key ) ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
if (
conflictingKeyDocument
&& conflictingKeyDocument . documentId !== documentPlan . source . documentId
) {
continue ;
}
const existingDocument = await tx
. select ( { id : documents.id } )
. from ( documents )
. where ( eq ( documents . id , documentPlan . source . documentId ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
if ( ! existingDocument ) {
await tx . insert ( documents ) . values ( {
id : documentPlan.source.documentId ,
companyId ,
title : documentPlan.source.title ,
format : documentPlan.source.format ,
latestBody : documentPlan.source.latestBody ,
latestRevisionId : documentPlan.latestRevisionId ,
latestRevisionNumber : documentPlan.latestRevisionNumber ,
createdByAgentId : documentPlan.targetCreatedByAgentId ,
createdByUserId : documentPlan.source.createdByUserId ,
updatedByAgentId : documentPlan.targetUpdatedByAgentId ,
updatedByUserId : documentPlan.source.updatedByUserId ,
createdAt : documentPlan.source.documentCreatedAt ,
updatedAt : documentPlan.source.documentUpdatedAt ,
} ) ;
await tx . insert ( issueDocuments ) . values ( {
id : documentPlan.source.id ,
companyId ,
issueId : documentPlan.source.issueId ,
documentId : documentPlan.source.documentId ,
key : documentPlan.source.key ,
createdAt : documentPlan.source.linkCreatedAt ,
updatedAt : documentPlan.source.linkUpdatedAt ,
} ) ;
insertedDocuments += 1 ;
} else {
const existingLink = await tx
. select ( { id : issueDocuments.id } )
. from ( issueDocuments )
. where ( eq ( issueDocuments . documentId , documentPlan . source . documentId ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
if ( ! existingLink ) {
await tx . insert ( issueDocuments ) . values ( {
id : documentPlan.source.id ,
companyId ,
issueId : documentPlan.source.issueId ,
documentId : documentPlan.source.documentId ,
key : documentPlan.source.key ,
createdAt : documentPlan.source.linkCreatedAt ,
updatedAt : documentPlan.source.linkUpdatedAt ,
} ) ;
} else {
await tx
. update ( issueDocuments )
. set ( {
issueId : documentPlan.source.issueId ,
key : documentPlan.source.key ,
updatedAt : documentPlan.source.linkUpdatedAt ,
} )
. where ( eq ( issueDocuments . documentId , documentPlan . source . documentId ) ) ;
}
await tx
. update ( documents )
. set ( {
title : documentPlan.source.title ,
format : documentPlan.source.format ,
latestBody : documentPlan.source.latestBody ,
latestRevisionId : documentPlan.latestRevisionId ,
latestRevisionNumber : documentPlan.latestRevisionNumber ,
updatedByAgentId : documentPlan.targetUpdatedByAgentId ,
updatedByUserId : documentPlan.source.updatedByUserId ,
updatedAt : documentPlan.source.documentUpdatedAt ,
} )
. where ( eq ( documents . id , documentPlan . source . documentId ) ) ;
mergedDocuments += 1 ;
}
const existingRevisionIds = new Set (
(
await tx
. select ( { id : documentRevisions.id } )
. from ( documentRevisions )
. where ( eq ( documentRevisions . documentId , documentPlan . source . documentId ) )
) . map ( ( row ) = > row . id ) ,
) ;
for ( const revisionPlan of documentPlan . revisionsToInsert ) {
if ( existingRevisionIds . has ( revisionPlan . source . id ) ) continue ;
await tx . insert ( documentRevisions ) . values ( {
id : revisionPlan.source.id ,
companyId ,
documentId : documentPlan.source.documentId ,
revisionNumber : revisionPlan.targetRevisionNumber ,
body : revisionPlan.source.body ,
changeSummary : revisionPlan.source.changeSummary ,
createdByAgentId : revisionPlan.targetCreatedByAgentId ,
createdByUserId : revisionPlan.source.createdByUserId ,
createdAt : revisionPlan.source.createdAt ,
} ) ;
insertedDocumentRevisions += 1 ;
}
}
const attachmentCandidates = input . plan . attachmentPlans . filter (
( plan ) : plan is PlannedAttachmentInsert = > plan . action === "insert" ,
) ;
const existingAttachmentIds = new Set (
(
await tx
. select ( { id : issueAttachments.id } )
. from ( issueAttachments )
. where ( eq ( issueAttachments . companyId , companyId ) )
) . map ( ( row ) = > row . id ) ,
) ;
let insertedAttachments = 0 ;
2026-03-20 16:06:41 -05:00
let skippedMissingAttachmentObjects = 0 ;
2026-03-20 15:59:41 -05:00
for ( const attachment of attachmentCandidates ) {
if ( existingAttachmentIds . has ( attachment . source . id ) ) continue ;
const parentExists = await tx
. select ( { id : issues.id } )
. from ( issues )
. where ( and ( eq ( issues . id , attachment . source . issueId ) , eq ( issues . companyId , companyId ) ) )
. then ( ( rows ) = > rows [ 0 ] ? ? null ) ;
if ( ! parentExists ) continue ;
2026-03-20 16:06:41 -05:00
const body = await readSourceAttachmentBody (
2026-03-20 16:12:10 -05:00
input . sourceStorages ,
2026-03-20 16:06:41 -05:00
companyId ,
attachment . source . objectKey ,
) ;
if ( ! body ) {
skippedMissingAttachmentObjects += 1 ;
continue ;
}
2026-03-20 15:59:41 -05:00
await input . targetStorage . putObject (
companyId ,
attachment . source . objectKey ,
body ,
attachment . source . contentType ,
) ;
await tx . insert ( assets ) . values ( {
id : attachment.source.assetId ,
companyId ,
provider : attachment.source.provider ,
objectKey : attachment.source.objectKey ,
contentType : attachment.source.contentType ,
byteSize : attachment.source.byteSize ,
sha256 : attachment.source.sha256 ,
originalFilename : attachment.source.originalFilename ,
createdByAgentId : attachment.targetCreatedByAgentId ,
createdByUserId : attachment.source.createdByUserId ,
createdAt : attachment.source.assetCreatedAt ,
updatedAt : attachment.source.assetUpdatedAt ,
} ) ;
await tx . insert ( issueAttachments ) . values ( {
id : attachment.source.id ,
companyId ,
issueId : attachment.source.issueId ,
assetId : attachment.source.assetId ,
issueCommentId : attachment.targetIssueCommentId ,
createdAt : attachment.source.attachmentCreatedAt ,
updatedAt : attachment.source.attachmentUpdatedAt ,
} ) ;
insertedAttachments += 1 ;
2026-03-20 15:02:24 -05:00
}
return {
2026-03-21 17:09:46 -05:00
insertedProjects ,
insertedProjectWorkspaces ,
2026-03-20 15:59:41 -05:00
insertedIssues ,
insertedComments ,
insertedDocuments ,
mergedDocuments ,
insertedDocumentRevisions ,
insertedAttachments ,
2026-03-20 16:06:41 -05:00
skippedMissingAttachmentObjects ,
2026-03-20 15:02:24 -05:00
insertedIssueIdentifiers ,
} ;
} ) ;
}
2026-03-20 15:17:51 -05:00
export async function worktreeMergeHistoryCommand ( sourceArg : string | undefined , opts : WorktreeMergeHistoryOptions ) : Promise < void > {
2026-03-20 15:02:24 -05:00
if ( opts . apply && opts . dry ) {
throw new Error ( "Use either --apply or --dry, not both." ) ;
}
2026-03-20 15:39:02 -05:00
if ( sourceArg && opts . from ) {
throw new Error ( "Use either the positional source argument or --from, not both." ) ;
2026-03-20 15:02:24 -05:00
}
2026-03-20 15:39:02 -05:00
const targetEndpoint = opts . to
? resolveWorktreeEndpointFromSelector ( opts . to , { allowCurrent : true } )
: resolveCurrentEndpoint ( ) ;
const sourceEndpoint = opts . from
? resolveWorktreeEndpointFromSelector ( opts . from , { allowCurrent : true } )
: sourceArg
? resolveWorktreeEndpointFromSelector ( sourceArg , { allowCurrent : true } )
: await promptForSourceEndpoint ( targetEndpoint . rootPath ) ;
if ( path . resolve ( sourceEndpoint . configPath ) === path . resolve ( targetEndpoint . configPath ) ) {
throw new Error ( "Source and target Paperclip configs are the same. Choose different --from/--to worktrees." ) ;
2026-03-20 15:02:24 -05:00
}
const scopes = parseWorktreeMergeScopes ( opts . scope ) ;
2026-03-20 15:39:02 -05:00
const sourceHandle = await openConfiguredDb ( sourceEndpoint . configPath ) ;
const targetHandle = await openConfiguredDb ( targetEndpoint . configPath ) ;
2026-03-20 16:12:10 -05:00
const sourceStorages = resolveAttachmentLookupStorages ( {
sourceEndpoint ,
targetEndpoint ,
} ) ;
2026-03-20 15:59:41 -05:00
const targetStorage = openConfiguredStorage ( targetEndpoint . configPath ) ;
2026-03-20 15:02:24 -05:00
try {
const company = await resolveMergeCompany ( {
sourceDb : sourceHandle.db ,
targetDb : targetHandle.db ,
selector : opts.company ,
} ) ;
2026-03-20 15:13:35 -05:00
let collected = await collectMergePlan ( {
2026-03-20 15:02:24 -05:00
sourceDb : sourceHandle.db ,
targetDb : targetHandle.db ,
company ,
scopes ,
} ) ;
2026-03-20 15:13:35 -05:00
if ( ! opts . yes ) {
2026-03-21 17:09:46 -05:00
const projectSelections = await promptForProjectMappings ( {
2026-03-20 15:13:35 -05:00
plan : collected.plan ,
sourceProjects : collected.sourceProjects ,
targetProjects : collected.targetProjects ,
} ) ;
2026-03-21 17:09:46 -05:00
if (
projectSelections . importProjectIds . length > 0
|| Object . keys ( projectSelections . projectIdOverrides ) . length > 0
) {
2026-03-20 15:13:35 -05:00
collected = await collectMergePlan ( {
sourceDb : sourceHandle.db ,
targetDb : targetHandle.db ,
company ,
scopes ,
2026-03-21 17:09:46 -05:00
importProjectIds : projectSelections.importProjectIds ,
projectIdOverrides : projectSelections.projectIdOverrides ,
2026-03-20 15:13:35 -05:00
} ) ;
}
}
2026-03-20 15:02:24 -05:00
console . log ( renderMergePlan ( collected . plan , {
2026-03-20 15:39:02 -05:00
sourcePath : ` ${ sourceEndpoint . label } ( ${ sourceEndpoint . rootPath } ) ` ,
targetPath : ` ${ targetEndpoint . label } ( ${ targetEndpoint . rootPath } ) ` ,
2026-03-20 15:02:24 -05:00
unsupportedRunCount : collected.unsupportedRunCount ,
} ) ) ;
if ( ! opts . apply ) {
return ;
}
const confirmed = opts . yes
? true
: await p . confirm ( {
2026-03-20 15:39:02 -05:00
message : ` Import ${ collected . plan . counts . issuesToInsert } issues and ${ collected . plan . counts . commentsToInsert } comments from ${ sourceEndpoint . label } into ${ targetEndpoint . label } ? ` ,
2026-03-20 15:02:24 -05:00
initialValue : false ,
} ) ;
if ( p . isCancel ( confirmed ) || ! confirmed ) {
p . log . warn ( "Import cancelled." ) ;
return ;
}
const applied = await applyMergePlan ( {
2026-03-20 16:12:10 -05:00
sourceStorages ,
2026-03-20 15:59:41 -05:00
targetStorage ,
2026-03-20 15:02:24 -05:00
targetDb : targetHandle.db ,
company ,
plan : collected.plan ,
} ) ;
2026-03-20 16:06:41 -05:00
if ( applied . skippedMissingAttachmentObjects > 0 ) {
p . log . warn (
` Skipped ${ applied . skippedMissingAttachmentObjects } attachments whose source files were missing from storage. ` ,
) ;
}
2026-03-20 15:02:24 -05:00
p . outro (
pc . green (
2026-03-21 17:09:46 -05:00
` Imported ${ applied . insertedProjects } projects ( ${ applied . insertedProjectWorkspaces } workspaces), ${ applied . insertedIssues } issues, ${ applied . insertedComments } comments, ${ applied . insertedDocuments } documents ( ${ applied . insertedDocumentRevisions } revisions, ${ applied . mergedDocuments } merged), and ${ applied . insertedAttachments } attachments into ${ company . issuePrefix } . ` ,
2026-03-20 15:02:24 -05:00
) ,
) ;
} finally {
await targetHandle . stop ( ) ;
await sourceHandle . stop ( ) ;
}
}
[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
async function runWorktreeReseed ( opts : WorktreeReseedOptions ) : Promise < void > {
2026-04-07 07:48:22 -05:00
const seedMode = opts . seedMode ? ? "full" ;
if ( ! isWorktreeSeedMode ( seedMode ) ) {
throw new Error ( ` Unsupported seed mode " ${ seedMode } ". Expected one of: minimal, full. ` ) ;
}
const targetEndpoint = opts . to
? resolveWorktreeEndpointFromSelector ( opts . to , { allowCurrent : true } )
: resolveCurrentEndpoint ( ) ;
const source = resolveWorktreeReseedSource ( opts ) ;
if ( path . resolve ( source . configPath ) === path . resolve ( targetEndpoint . configPath ) ) {
throw new Error ( "Source and target Paperclip configs are the same. Choose different --from/--to values." ) ;
}
if ( ! existsSync ( source . configPath ) ) {
throw new Error ( ` Source config not found at ${ source . configPath } . ` ) ;
}
const targetConfig = readConfig ( targetEndpoint . configPath ) ;
if ( ! targetConfig ) {
throw new Error ( ` Target config not found at ${ targetEndpoint . configPath } . ` ) ;
}
const sourceConfig = readConfig ( source . configPath ) ;
if ( ! sourceConfig ) {
throw new Error ( ` Source config not found at ${ source . configPath } . ` ) ;
}
const targetPaths = resolveWorktreeReseedTargetPaths ( {
configPath : targetEndpoint.configPath ,
rootPath : targetEndpoint.rootPath ,
} ) ;
const runningTargetPid = resolveRunningEmbeddedPostgresPid ( targetConfig ) ;
if ( runningTargetPid && ! opts . allowLiveTarget ) {
throw new Error (
` Target worktree database appears to be running (pid ${ runningTargetPid } ). Stop Paperclip in ${ targetEndpoint . rootPath } before reseeding, or re-run with --allow-live-target if you want to override this guard. ` ,
) ;
}
const confirmed = opts . yes
? true
: await p . confirm ( {
message : ` Overwrite the isolated Paperclip DB for ${ targetEndpoint . label } from ${ source . label } using ${ seedMode } seed mode? ` ,
initialValue : false ,
} ) ;
if ( p . isCancel ( confirmed ) || ! confirmed ) {
p . log . warn ( "Reseed cancelled." ) ;
return ;
}
if ( runningTargetPid && opts . allowLiveTarget ) {
p . log . warning ( ` Proceeding even though the target embedded PostgreSQL appears to be running (pid ${ runningTargetPid } ). ` ) ;
}
const spinner = p . spinner ( ) ;
spinner . start ( ` Reseeding ${ targetEndpoint . label } from ${ source . label } ( ${ seedMode } )... ` ) ;
try {
const seeded = await seedWorktreeDatabase ( {
sourceConfigPath : source.configPath ,
sourceConfig ,
targetConfig ,
targetPaths ,
instanceId : targetPaths.instanceId ,
seedMode ,
[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
preserveLiveWork : opts.preserveLiveWork ,
2026-04-07 07:48:22 -05:00
} ) ;
spinner . stop ( ` Reseeded ${ targetEndpoint . label } ( ${ seedMode } ). ` ) ;
p . log . message ( pc . dim ( ` Source: ${ source . configPath } ` ) ) ;
p . log . message ( pc . dim ( ` Target: ${ targetEndpoint . configPath } ` ) ) ;
p . log . message ( pc . dim ( ` Seed snapshot: ${ seeded . backupSummary } ` ) ) ;
[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
if ( opts . preserveLiveWork ) {
p . log . warning ( "Preserved copied live work; this worktree instance may auto-run source-instance assignments." ) ;
} else {
p . log . message (
pc . dim ( ` Seed execution quarantine: ${ formatSeededWorktreeExecutionQuarantineSummary ( seeded . executionQuarantine ) } ` ) ,
) ;
}
p . log . message ( pc . dim ( ` Paused scheduled routines: ${ seeded . pausedScheduledRoutines } ` ) ) ;
2026-04-07 07:48:22 -05:00
for ( const rebound of seeded . reboundWorkspaces ) {
p . log . message (
pc . dim ( ` Rebound workspace ${ rebound . name } : ${ rebound . fromCwd } -> ${ rebound . toCwd } ` ) ,
) ;
}
p . outro ( pc . green ( ` Reseed complete for ${ targetEndpoint . label } . ` ) ) ;
} catch ( error ) {
spinner . stop ( pc . red ( "Failed to reseed worktree database." ) ) ;
throw error ;
}
}
[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
export async function worktreeReseedCommand ( opts : WorktreeReseedOptions ) : Promise < void > {
printPaperclipCliBanner ( ) ;
p . intro ( pc . bgCyan ( pc . black ( " paperclipai worktree reseed " ) ) ) ;
await runWorktreeReseed ( opts ) ;
}
export async function worktreeRepairCommand ( opts : WorktreeRepairOptions ) : Promise < void > {
printPaperclipCliBanner ( ) ;
p . intro ( pc . bgCyan ( pc . black ( " paperclipai worktree repair " ) ) ) ;
const seedMode = opts . seedMode ? ? "minimal" ;
if ( ! isWorktreeSeedMode ( seedMode ) ) {
throw new Error ( ` Unsupported seed mode " ${ seedMode } ". Expected one of: minimal, full. ` ) ;
}
const target = await ensureRepairTargetWorktree ( {
selector : nonEmpty ( opts . branch ) ? ? undefined ,
seedMode ,
opts ,
} ) ;
if ( ! target ) {
p . log . warn ( "Current checkout is the primary repo worktree. Pass --branch to create or repair a linked worktree." ) ;
p . outro ( pc . yellow ( "No worktree repaired." ) ) ;
return ;
}
const source = resolveWorktreeRepairSource ( opts ) ;
if ( ! existsSync ( source . configPath ) ) {
throw new Error ( ` Source config not found at ${ source . configPath } . ` ) ;
}
if ( path . resolve ( source . configPath ) === path . resolve ( target . configPath ) ) {
throw new Error ( "Source and target Paperclip configs are the same. Use --from-config/--from-instance to point repair at a different source." ) ;
}
const targetConfig = existsSync ( target . configPath ) ? readConfig ( target . configPath ) : null ;
const targetEnvEntries = readPaperclipEnvEntries ( resolvePaperclipEnvFile ( target . configPath ) ) ;
const targetHasWorktreeEnv = Boolean (
nonEmpty ( targetEnvEntries . PAPERCLIP_HOME ) && nonEmpty ( targetEnvEntries . PAPERCLIP_INSTANCE_ID ) ,
) ;
if ( targetConfig && targetHasWorktreeEnv && opts . noSeed ) {
p . log . message ( pc . dim ( ` Target ${ target . label } already has worktree-local config/env. Skipping reseed because --no-seed was passed. ` ) ) ;
p . outro ( pc . green ( ` Worktree metadata already looks healthy for ${ target . label } . ` ) ) ;
return ;
}
if ( targetConfig && targetHasWorktreeEnv ) {
await runWorktreeReseed ( {
fromConfig : source.configPath ,
to : target.rootPath ,
seedMode ,
[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
preserveLiveWork : opts.preserveLiveWork ,
[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
yes : true ,
allowLiveTarget : opts.allowLiveTarget ,
} ) ;
return ;
}
const repairInstanceId = sanitizeWorktreeInstanceId ( path . basename ( target . rootPath ) ) ;
const repairPaths = resolveWorktreeLocalPaths ( {
cwd : target.rootPath ,
homeDir : resolveWorktreeHome ( opts . home ) ,
instanceId : repairInstanceId ,
} ) ;
const runningTargetPid = readRunningPostmasterPid ( path . resolve ( repairPaths . embeddedPostgresDataDir , "postmaster.pid" ) ) ;
if ( runningTargetPid && ! opts . allowLiveTarget ) {
throw new Error (
` Target worktree database appears to be running (pid ${ runningTargetPid } ). Stop Paperclip in ${ target . rootPath } before repairing, or re-run with --allow-live-target if you want to override this guard. ` ,
) ;
}
if ( runningTargetPid && opts . allowLiveTarget ) {
p . log . warning ( ` Proceeding even though the target embedded PostgreSQL appears to be running (pid ${ runningTargetPid } ). ` ) ;
}
const originalCwd = process . cwd ( ) ;
try {
process . chdir ( target . rootPath ) ;
await runWorktreeInit ( {
home : opts.home ,
fromConfig : source.configPath ,
fromDataDir : opts.fromDataDir ,
fromInstance : opts.fromInstance ,
seed : opts.noSeed ? false : true ,
seedMode ,
[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
preserveLiveWork : opts.preserveLiveWork ,
[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
force : true ,
} ) ;
} finally {
process . chdir ( originalCwd ) ;
}
}
2026-03-10 10:08:13 -05:00
export function registerWorktreeCommands ( program : Command ) : void {
const worktree = program . command ( "worktree" ) . description ( "Worktree-local Paperclip instance helpers" ) ;
2026-03-10 16:52:26 -05:00
program
. command ( "worktree:make" )
. description ( "Create ~/NAME as a git worktree, then initialize an isolated Paperclip instance inside it" )
2026-03-13 07:24:39 -05:00
. argument ( "<name>" , "Worktree name — auto-prefixed with paperclip- if needed (created at ~/paperclip-NAME)" )
. option ( "--start-point <ref>" , "Remote ref to base the new branch on (env: PAPERCLIP_WORKTREE_START_POINT)" )
2026-03-10 16:52:26 -05:00
. option ( "--instance <id>" , "Explicit isolated instance id" )
2026-03-13 07:24:39 -05:00
. option ( "--home <path>" , ` Home root for worktree instances (env: PAPERCLIP_WORKTREES_DIR, default: ${ DEFAULT_WORKTREE_HOME } ) ` )
2026-03-10 16:52:26 -05:00
. option ( "--from-config <path>" , "Source config.json to seed from" )
. option ( "--from-data-dir <path>" , "Source PAPERCLIP_HOME used when deriving the source config" )
. option ( "--from-instance <id>" , "Source instance id when deriving the source config" , "default" )
. option ( "--server-port <port>" , "Preferred server port" , ( value ) = > Number ( value ) )
. option ( "--db-port <port>" , "Preferred embedded Postgres port" , ( value ) = > Number ( value ) )
. option ( "--seed-mode <mode>" , "Seed profile: minimal or full (default: minimal)" , "minimal" )
[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
. option ( "--preserve-live-work" , "Do not quarantine copied agent timers or assigned open issues in the seeded worktree" , false )
2026-03-10 16:52:26 -05:00
. option ( "--no-seed" , "Skip database seeding from the source instance" )
. option ( "--force" , "Replace existing repo-local config and isolated instance data" , false )
. action ( worktreeMakeCommand ) ;
2026-03-10 10:08:13 -05:00
worktree
. command ( "init" )
. description ( "Create repo-local config/env and an isolated instance for this worktree" )
. option ( "--name <name>" , "Display name used to derive the instance id" )
. option ( "--instance <id>" , "Explicit isolated instance id" )
2026-03-13 07:24:39 -05:00
. option ( "--home <path>" , ` Home root for worktree instances (env: PAPERCLIP_WORKTREES_DIR, default: ${ DEFAULT_WORKTREE_HOME } ) ` )
2026-03-10 10:08:13 -05:00
. option ( "--from-config <path>" , "Source config.json to seed from" )
. option ( "--from-data-dir <path>" , "Source PAPERCLIP_HOME used when deriving the source config" )
. option ( "--from-instance <id>" , "Source instance id when deriving the source config" , "default" )
. option ( "--server-port <port>" , "Preferred server port" , ( value ) = > Number ( value ) )
. option ( "--db-port <port>" , "Preferred embedded Postgres port" , ( value ) = > Number ( value ) )
2026-03-10 07:41:01 -05:00
. option ( "--seed-mode <mode>" , "Seed profile: minimal or full (default: minimal)" , "minimal" )
[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
. option ( "--preserve-live-work" , "Do not quarantine copied agent timers or assigned open issues in the seeded worktree" , false )
2026-03-10 10:08:13 -05:00
. option ( "--no-seed" , "Skip database seeding from the source instance" )
. option ( "--force" , "Replace existing repo-local config and isolated instance data" , false )
. action ( worktreeInitCommand ) ;
worktree
. command ( "env" )
. description ( "Print shell exports for the current worktree-local Paperclip instance" )
. option ( "-c, --config <path>" , "Path to config file" )
. option ( "--json" , "Print JSON instead of shell exports" )
. action ( worktreeEnvCommand ) ;
2026-03-13 07:24:39 -05:00
2026-03-20 15:17:51 -05:00
program
. command ( "worktree:list" )
. description ( "List git worktrees visible from this repo and whether they look like Paperclip worktrees" )
. option ( "--json" , "Print JSON instead of text output" )
. action ( worktreeListCommand ) ;
2026-03-20 15:02:24 -05:00
program
. command ( "worktree:merge-history" )
. description ( "Preview or import issue/comment history from another worktree into the current instance" )
2026-03-20 15:39:02 -05:00
. argument ( "[source]" , "Optional source worktree path, directory name, or branch name (back-compat alias for --from)" )
. option ( "--from <worktree>" , "Source worktree path, directory name, branch name, or current" )
. option ( "--to <worktree>" , "Target worktree path, directory name, branch name, or current (defaults to current)" )
. option ( "--company <id-or-prefix>" , "Shared company id or issue prefix inside the chosen source/target instances" )
2026-03-20 15:02:24 -05:00
. option ( "--scope <items>" , "Comma-separated scopes to import (issues, comments)" , "issues,comments" )
. option ( "--apply" , "Apply the import after previewing the plan" , false )
. option ( "--dry" , "Preview only and do not import anything" , false )
. option ( "--yes" , "Skip the interactive confirmation prompt when applying" , false )
. action ( worktreeMergeHistoryCommand ) ;
2026-04-07 07:48:22 -05:00
worktree
. command ( "reseed" )
. description ( "Re-seed an existing worktree-local instance from another Paperclip instance or worktree" )
. option ( "--from <worktree>" , "Source worktree path, directory name, branch name, or current" )
. option ( "--to <worktree>" , "Target worktree path, directory name, branch name, or current (defaults to current)" )
. option ( "--from-config <path>" , "Source config.json to seed from" )
. option ( "--from-data-dir <path>" , "Source PAPERCLIP_HOME used when deriving the source config" )
. option ( "--from-instance <id>" , "Source instance id when deriving the source config" )
. option ( "--seed-mode <mode>" , "Seed profile: minimal or full (default: full)" , "full" )
[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
. option ( "--preserve-live-work" , "Do not quarantine copied agent timers or assigned open issues in the seeded worktree" , false )
2026-04-07 07:48:22 -05:00
. option ( "--yes" , "Skip the destructive confirmation prompt" , false )
. option ( "--allow-live-target" , "Override the guard that requires the target worktree DB to be stopped first" , false )
. action ( worktreeReseedCommand ) ;
[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
worktree
. command ( "repair" )
. description ( "Create or repair a linked worktree-local Paperclip instance without touching the primary checkout" )
. option ( "--branch <name>" , "Existing branch/worktree selector to repair, or a branch name to create under .paperclip/worktrees" )
. option ( "--home <path>" , ` Home root for worktree instances (env: PAPERCLIP_WORKTREES_DIR, default: ${ DEFAULT_WORKTREE_HOME } ) ` )
. option ( "--from-config <path>" , "Source config.json to seed from" )
. option ( "--from-data-dir <path>" , "Source PAPERCLIP_HOME used when deriving the source config" )
. option ( "--from-instance <id>" , "Source instance id when deriving the source config (default: default)" )
. option ( "--seed-mode <mode>" , "Seed profile: minimal or full (default: minimal)" , "minimal" )
[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
. option ( "--preserve-live-work" , "Do not quarantine copied agent timers or assigned open issues in the seeded worktree" , false )
[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
. option ( "--no-seed" , "Repair metadata only and skip reseeding when bootstrapping a missing worktree config" , false )
. option ( "--allow-live-target" , "Override the guard that requires the target worktree DB to be stopped first" , false )
. action ( worktreeRepairCommand ) ;
2026-03-13 07:24:39 -05:00
program
. command ( "worktree:cleanup" )
. description ( "Safely remove a worktree, its branch, and its isolated instance data" )
. argument ( "<name>" , "Worktree name — auto-prefixed with paperclip- if needed" )
. option ( "--instance <id>" , "Explicit instance id (if different from the worktree name)" )
. option ( "--home <path>" , ` Home root for worktree instances (env: PAPERCLIP_WORKTREES_DIR, default: ${ DEFAULT_WORKTREE_HOME } ) ` )
. option ( "--force" , "Bypass safety checks (uncommitted changes, unique commits)" , false )
. action ( worktreeCleanupCommand ) ;
2026-03-10 10:08:13 -05:00
}