2026-02-16 13:31:47 -06:00
|
|
|
import { z } from "zod";
|
2026-04-06 08:40:38 -05:00
|
|
|
import {
|
|
|
|
|
ISSUE_EXECUTION_DECISION_OUTCOMES,
|
|
|
|
|
ISSUE_EXECUTION_POLICY_MODES,
|
|
|
|
|
ISSUE_EXECUTION_STAGE_TYPES,
|
|
|
|
|
ISSUE_EXECUTION_STATE_STATUSES,
|
|
|
|
|
ISSUE_PRIORITIES,
|
|
|
|
|
ISSUE_STATUSES,
|
|
|
|
|
} from "../constants.js";
|
2026-02-16 13:31:47 -06:00
|
|
|
|
2026-04-02 12:09:02 -05:00
|
|
|
export const ISSUE_EXECUTION_WORKSPACE_PREFERENCES = [
|
|
|
|
|
"inherit",
|
|
|
|
|
"shared_workspace",
|
|
|
|
|
"isolated_workspace",
|
|
|
|
|
"operator_branch",
|
|
|
|
|
"reuse_existing",
|
|
|
|
|
"agent_default",
|
|
|
|
|
] as const;
|
|
|
|
|
|
2026-03-10 09:03:31 -05:00
|
|
|
const executionWorkspaceStrategySchema = z
|
|
|
|
|
.object({
|
2026-03-13 17:12:25 -05:00
|
|
|
type: z.enum(["project_primary", "git_worktree", "adapter_managed", "cloud_sandbox"]).optional(),
|
2026-03-10 09:03:31 -05:00
|
|
|
baseRef: z.string().optional().nullable(),
|
|
|
|
|
branchTemplate: z.string().optional().nullable(),
|
|
|
|
|
worktreeParentDir: z.string().optional().nullable(),
|
2026-03-10 12:42:36 -05:00
|
|
|
provisionCommand: z.string().optional().nullable(),
|
|
|
|
|
teardownCommand: z.string().optional().nullable(),
|
2026-03-10 09:03:31 -05:00
|
|
|
})
|
|
|
|
|
.strict();
|
|
|
|
|
|
|
|
|
|
export const issueExecutionWorkspaceSettingsSchema = z
|
|
|
|
|
.object({
|
2026-04-02 12:09:02 -05:00
|
|
|
mode: z.enum(ISSUE_EXECUTION_WORKSPACE_PREFERENCES).optional(),
|
2026-03-10 09:03:31 -05:00
|
|
|
workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(),
|
|
|
|
|
workspaceRuntime: z.record(z.unknown()).optional().nullable(),
|
|
|
|
|
})
|
|
|
|
|
.strict();
|
|
|
|
|
|
2026-02-26 10:32:44 -06:00
|
|
|
export const issueAssigneeAdapterOverridesSchema = z
|
|
|
|
|
.object({
|
|
|
|
|
adapterConfig: z.record(z.unknown()).optional(),
|
|
|
|
|
useProjectWorkspace: z.boolean().optional(),
|
|
|
|
|
})
|
|
|
|
|
.strict();
|
|
|
|
|
|
2026-04-06 08:40:38 -05:00
|
|
|
const issueExecutionStagePrincipalBaseSchema = z.object({
|
|
|
|
|
type: z.enum(["agent", "user"]),
|
|
|
|
|
agentId: z.string().uuid().optional().nullable(),
|
|
|
|
|
userId: z.string().optional().nullable(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const issueExecutionStagePrincipalSchema = issueExecutionStagePrincipalBaseSchema
|
|
|
|
|
.superRefine((value, ctx) => {
|
|
|
|
|
if (value.type === "agent") {
|
|
|
|
|
if (!value.agentId) {
|
|
|
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants require agentId", path: ["agentId"] });
|
|
|
|
|
}
|
|
|
|
|
if (value.userId) {
|
|
|
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants cannot set userId", path: ["userId"] });
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!value.userId) {
|
|
|
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants require userId", path: ["userId"] });
|
|
|
|
|
}
|
|
|
|
|
if (value.agentId) {
|
|
|
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants cannot set agentId", path: ["agentId"] });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const issueExecutionStageParticipantSchema = issueExecutionStagePrincipalBaseSchema.extend({
|
|
|
|
|
id: z.string().uuid().optional(),
|
|
|
|
|
}).superRefine((value, ctx) => {
|
|
|
|
|
if (value.type === "agent") {
|
|
|
|
|
if (!value.agentId) {
|
|
|
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants require agentId", path: ["agentId"] });
|
|
|
|
|
}
|
|
|
|
|
if (value.userId) {
|
|
|
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Agent participants cannot set userId", path: ["userId"] });
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!value.userId) {
|
|
|
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants require userId", path: ["userId"] });
|
|
|
|
|
}
|
|
|
|
|
if (value.agentId) {
|
|
|
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "User participants cannot set agentId", path: ["agentId"] });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const issueExecutionStageSchema = z.object({
|
|
|
|
|
id: z.string().uuid().optional(),
|
|
|
|
|
type: z.enum(ISSUE_EXECUTION_STAGE_TYPES),
|
2026-04-07 17:07:10 -05:00
|
|
|
approvalsNeeded: z.literal(1).optional().default(1),
|
2026-04-06 08:40:38 -05:00
|
|
|
participants: z.array(issueExecutionStageParticipantSchema).default([]),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const issueExecutionPolicySchema = z.object({
|
|
|
|
|
mode: z.enum(ISSUE_EXECUTION_POLICY_MODES).optional().default("normal"),
|
|
|
|
|
commentRequired: z.boolean().optional().default(true),
|
|
|
|
|
stages: z.array(issueExecutionStageSchema).default([]),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const issueExecutionStateSchema = z.object({
|
|
|
|
|
status: z.enum(ISSUE_EXECUTION_STATE_STATUSES),
|
|
|
|
|
currentStageId: z.string().uuid().nullable(),
|
|
|
|
|
currentStageIndex: z.number().int().nonnegative().nullable(),
|
|
|
|
|
currentStageType: z.enum(ISSUE_EXECUTION_STAGE_TYPES).nullable(),
|
|
|
|
|
currentParticipant: issueExecutionStagePrincipalSchema.nullable(),
|
|
|
|
|
returnAssignee: issueExecutionStagePrincipalSchema.nullable(),
|
|
|
|
|
completedStageIds: z.array(z.string().uuid()).default([]),
|
|
|
|
|
lastDecisionId: z.string().uuid().nullable(),
|
|
|
|
|
lastDecisionOutcome: z.enum(ISSUE_EXECUTION_DECISION_OUTCOMES).nullable(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-16 13:31:47 -06:00
|
|
|
export const createIssueSchema = z.object({
|
Expand data model with companies, approvals, costs, and heartbeats
Add new DB schemas: companies, agent_api_keys, approvals, cost_events,
heartbeat_runs, issue_comments. Add corresponding shared types and
validators. Update existing schemas (agents, goals, issues, projects)
with new fields for company association, budgets, and richer metadata.
Generate initial Drizzle migration. Update seed data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:22 -06:00
|
|
|
projectId: z.string().uuid().optional().nullable(),
|
2026-03-13 17:12:25 -05:00
|
|
|
projectWorkspaceId: z.string().uuid().optional().nullable(),
|
Expand data model with companies, approvals, costs, and heartbeats
Add new DB schemas: companies, agent_api_keys, approvals, cost_events,
heartbeat_runs, issue_comments. Add corresponding shared types and
validators. Update existing schemas (agents, goals, issues, projects)
with new fields for company association, budgets, and richer metadata.
Generate initial Drizzle migration. Update seed data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:22 -06:00
|
|
|
goalId: z.string().uuid().optional().nullable(),
|
|
|
|
|
parentId: z.string().uuid().optional().nullable(),
|
2026-04-04 13:56:04 -05:00
|
|
|
blockedByIssueIds: z.array(z.string().uuid()).optional(),
|
2026-03-30 14:08:44 -05:00
|
|
|
inheritExecutionWorkspaceFromIssueId: z.string().uuid().optional().nullable(),
|
2026-02-16 13:31:47 -06:00
|
|
|
title: z.string().min(1),
|
|
|
|
|
description: z.string().optional().nullable(),
|
|
|
|
|
status: z.enum(ISSUE_STATUSES).optional().default("backlog"),
|
|
|
|
|
priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"),
|
Expand data model with companies, approvals, costs, and heartbeats
Add new DB schemas: companies, agent_api_keys, approvals, cost_events,
heartbeat_runs, issue_comments. Add corresponding shared types and
validators. Update existing schemas (agents, goals, issues, projects)
with new fields for company association, budgets, and richer metadata.
Generate initial Drizzle migration. Update seed data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:22 -06:00
|
|
|
assigneeAgentId: z.string().uuid().optional().nullable(),
|
feat: add auth/access foundation - deps, DB schema, shared types, and config
Add Better Auth, drizzle-orm, @dnd-kit, and remark-gfm dependencies.
Introduce DB schema for auth tables (user, session, account, verification),
company memberships, instance user roles, permission grants, invites, and
join requests. Add assigneeUserId to issues. Extend shared config schema
with deployment mode/exposure/auth settings, add access types and validators,
and wire up new API path constants.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:40:16 -06:00
|
|
|
assigneeUserId: z.string().optional().nullable(),
|
Expand data model with companies, approvals, costs, and heartbeats
Add new DB schemas: companies, agent_api_keys, approvals, cost_events,
heartbeat_runs, issue_comments. Add corresponding shared types and
validators. Update existing schemas (agents, goals, issues, projects)
with new fields for company association, budgets, and richer metadata.
Generate initial Drizzle migration. Update seed data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:22 -06:00
|
|
|
requestDepth: z.number().int().nonnegative().optional().default(0),
|
|
|
|
|
billingCode: z.string().optional().nullable(),
|
2026-02-26 10:32:44 -06:00
|
|
|
assigneeAdapterOverrides: issueAssigneeAdapterOverridesSchema.optional().nullable(),
|
2026-04-06 08:40:38 -05:00
|
|
|
executionPolicy: issueExecutionPolicySchema.optional().nullable(),
|
2026-03-13 17:12:25 -05:00
|
|
|
executionWorkspaceId: z.string().uuid().optional().nullable(),
|
2026-04-02 12:09:02 -05:00
|
|
|
executionWorkspacePreference: z.enum(ISSUE_EXECUTION_WORKSPACE_PREFERENCES).optional().nullable(),
|
2026-03-10 09:03:31 -05:00
|
|
|
executionWorkspaceSettings: issueExecutionWorkspaceSettingsSchema.optional().nullable(),
|
2026-02-25 08:38:37 -06:00
|
|
|
labelIds: z.array(z.string().uuid()).optional(),
|
2026-02-16 13:31:47 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type CreateIssue = z.infer<typeof createIssueSchema>;
|
2026-02-25 08:38:37 -06:00
|
|
|
|
|
|
|
|
export const createIssueLabelSchema = z.object({
|
|
|
|
|
name: z.string().trim().min(1).max(48),
|
|
|
|
|
color: z.string().regex(/^#(?:[0-9a-fA-F]{6})$/, "Color must be a 6-digit hex value"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type CreateIssueLabel = z.infer<typeof createIssueLabelSchema>;
|
2026-02-16 13:31:47 -06:00
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
export const updateIssueSchema = createIssueSchema.partial().extend({
|
[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
|
|
|
assigneeAgentId: z.string().trim().min(1).optional().nullable(),
|
2026-02-17 20:46:12 -06:00
|
|
|
comment: z.string().min(1).optional(),
|
2026-03-20 15:46:01 -05:00
|
|
|
reopen: z.boolean().optional(),
|
2026-03-28 10:34:36 -05:00
|
|
|
interrupt: z.boolean().optional(),
|
2026-02-19 15:43:43 -06:00
|
|
|
hiddenAt: z.string().datetime().nullable().optional(),
|
2026-02-17 20:46:12 -06:00
|
|
|
});
|
2026-02-16 13:31:47 -06:00
|
|
|
|
|
|
|
|
export type UpdateIssue = z.infer<typeof updateIssueSchema>;
|
2026-03-10 09:03:31 -05:00
|
|
|
export type IssueExecutionWorkspaceSettings = z.infer<typeof issueExecutionWorkspaceSettingsSchema>;
|
Expand data model with companies, approvals, costs, and heartbeats
Add new DB schemas: companies, agent_api_keys, approvals, cost_events,
heartbeat_runs, issue_comments. Add corresponding shared types and
validators. Update existing schemas (agents, goals, issues, projects)
with new fields for company association, budgets, and richer metadata.
Generate initial Drizzle migration. Update seed data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:22 -06:00
|
|
|
|
|
|
|
|
export const checkoutIssueSchema = z.object({
|
|
|
|
|
agentId: z.string().uuid(),
|
|
|
|
|
expectedStatuses: z.array(z.enum(ISSUE_STATUSES)).nonempty(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type CheckoutIssue = z.infer<typeof checkoutIssueSchema>;
|
|
|
|
|
|
|
|
|
|
export const addIssueCommentSchema = z.object({
|
|
|
|
|
body: z.string().min(1),
|
2026-02-19 09:09:26 -06:00
|
|
|
reopen: z.boolean().optional(),
|
2026-03-02 16:43:59 -06:00
|
|
|
interrupt: z.boolean().optional(),
|
Expand data model with companies, approvals, costs, and heartbeats
Add new DB schemas: companies, agent_api_keys, approvals, cost_events,
heartbeat_runs, issue_comments. Add corresponding shared types and
validators. Update existing schemas (agents, goals, issues, projects)
with new fields for company association, budgets, and richer metadata.
Generate initial Drizzle migration. Update seed data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:07:22 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type AddIssueComment = z.infer<typeof addIssueCommentSchema>;
|
2026-02-19 13:02:25 -06:00
|
|
|
|
|
|
|
|
export const linkIssueApprovalSchema = z.object({
|
|
|
|
|
approvalId: z.string().uuid(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type LinkIssueApproval = z.infer<typeof linkIssueApprovalSchema>;
|
2026-02-20 10:31:56 -06:00
|
|
|
|
|
|
|
|
export const createIssueAttachmentMetadataSchema = z.object({
|
|
|
|
|
issueCommentId: z.string().uuid().optional().nullable(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type CreateIssueAttachmentMetadata = z.infer<typeof createIssueAttachmentMetadataSchema>;
|
2026-03-13 21:30:48 -05:00
|
|
|
|
|
|
|
|
export const ISSUE_DOCUMENT_FORMATS = ["markdown"] as const;
|
|
|
|
|
|
|
|
|
|
export const issueDocumentFormatSchema = z.enum(ISSUE_DOCUMENT_FORMATS);
|
|
|
|
|
|
|
|
|
|
export const issueDocumentKeySchema = z
|
|
|
|
|
.string()
|
|
|
|
|
.trim()
|
|
|
|
|
.min(1)
|
|
|
|
|
.max(64)
|
|
|
|
|
.regex(/^[a-z0-9][a-z0-9_-]*$/, "Document key must be lowercase letters, numbers, _ or -");
|
|
|
|
|
|
|
|
|
|
export const upsertIssueDocumentSchema = z.object({
|
|
|
|
|
title: z.string().trim().max(200).nullable().optional(),
|
|
|
|
|
format: issueDocumentFormatSchema,
|
2026-03-14 09:09:21 -05:00
|
|
|
body: z.string().max(524288),
|
2026-03-13 21:30:48 -05:00
|
|
|
changeSummary: z.string().trim().max(500).nullable().optional(),
|
|
|
|
|
baseRevisionId: z.string().uuid().nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-26 08:24:57 -05:00
|
|
|
export const restoreIssueDocumentRevisionSchema = z.object({});
|
|
|
|
|
|
2026-03-13 21:30:48 -05:00
|
|
|
export type IssueDocumentFormat = z.infer<typeof issueDocumentFormatSchema>;
|
|
|
|
|
export type UpsertIssueDocument = z.infer<typeof upsertIssueDocumentSchema>;
|
2026-03-26 08:24:57 -05:00
|
|
|
export type RestoreIssueDocumentRevision = z.infer<typeof restoreIssueDocumentRevisionSchema>;
|