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,
|
[codex] Add structured issue-thread interactions (#4244)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Operators supervise that work through issues, comments, approvals,
and the board UI.
> - Some agent proposals need structured board/user decisions, not
hidden markdown conventions or heavyweight governed approvals.
> - Issue-thread interactions already provide a natural thread-native
surface for proposed tasks and questions.
> - This pull request extends that surface with request confirmations,
richer interaction cards, and agent/plugin/MCP helpers.
> - The benefit is that plan approvals and yes/no decisions become
explicit, auditable, and resumable without losing the single-issue
workflow.
## What Changed
- Added persisted issue-thread interactions for suggested tasks,
structured questions, and request confirmations.
- Added board UI cards for interaction review, selection, question
answers, and accept/reject confirmation flows.
- Added MCP and plugin SDK helpers for creating interaction cards from
agents/plugins.
- Updated agent wake instructions, onboarding assets, Paperclip skill
docs, and public docs to prefer structured confirmations for
issue-scoped decisions.
- Rebased the branch onto `public-gh/master` and renumbered branch
migrations to `0063` and `0064`; the idempotency migration uses `ADD
COLUMN IF NOT EXISTS` for old branch users.
## Verification
- `git diff --check public-gh/master..HEAD`
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
packages/mcp-server/src/tools.test.ts
packages/shared/src/issue-thread-interactions.test.ts
ui/src/lib/issue-thread-interactions.test.ts
ui/src/lib/issue-chat-messages.test.ts
ui/src/components/IssueThreadInteractionCard.test.tsx
ui/src/components/IssueChatThread.test.tsx
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/issue-thread-interactions-service.test.ts
server/src/services/issue-thread-interactions.test.ts` -> 9 files / 79
tests passed
- `pnpm -r typecheck` -> passed, including `packages/db` migration
numbering check
## Risks
- Medium: this adds a new issue-thread interaction model across
db/shared/server/ui/plugin surfaces.
- Migration risk is reduced by placing this branch after current master
migrations (`0063`, `0064`) and making the idempotency column add
idempotent for users who applied the old branch numbering.
- UI interaction behavior is covered by component tests, but this PR
does not include browser screenshots.
> 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-class coding agent runtime. Exact model ID and
context window are not exposed in this Paperclip run; tool use and local
shell/code execution were enabled.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-21 20:15:11 -05:00
|
|
|
ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES,
|
|
|
|
|
ISSUE_THREAD_INTERACTION_KINDS,
|
|
|
|
|
ISSUE_THREAD_INTERACTION_STATUSES,
|
2026-04-06 08:40:38 -05:00
|
|
|
} 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
|
|
|
|
[codex] Add run liveness continuations (#4083)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Heartbeat runs are the control-plane record of each agent execution
window.
> - Long-running local agents can exhaust context or stop while still
holding useful next-step state.
> - Operators need that stop reason, next action, and continuation path
to be durable and visible.
> - This pull request adds run liveness metadata, continuation
summaries, and UI surfaces for issue run ledgers.
> - The benefit is that interrupted or long-running work can resume with
clearer context instead of losing the agent's last useful handoff.
## What Changed
- Added heartbeat-run liveness fields, continuation attempt tracking,
and an idempotent `0058` migration.
- Added server services and tests for run liveness, continuation
summaries, stop metadata, and activity backfill.
- Wired local and HTTP adapters to surface continuation/liveness context
through shared adapter utilities.
- Added shared constants, validators, and heartbeat types for liveness
continuation state.
- Added issue-detail UI surfaces for continuation handoffs and the run
ledger, with component tests.
- Updated agent runtime docs, heartbeat protocol docs, prompt guidance,
onboarding assets, and skills instructions to explain continuation
behavior.
- Addressed Greptile feedback by scoping document evidence by run,
excluding system continuation-summary documents from liveness evidence,
importing shared liveness types, surfacing hidden ledger run counts,
documenting bounded retry behavior, and moving run-ledger liveness
backfill off the request path.
## Verification
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/run-continuations.test.ts
server/src/__tests__/run-liveness.test.ts
server/src/__tests__/activity-service.test.ts
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-continuation-summary.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/components/IssueDocumentsSection.test.tsx`
- `pnpm --filter @paperclipai/db build`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/run-continuations.test.ts
ui/src/components/IssueRunLedger.test.tsx`
- `pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts -t "treats a
plan document update"`
- `pnpm exec vitest run server/src/__tests__/activity-service.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts -t "activity
service|treats a plan document update"`
- Remote PR checks on head `e53b1a1d`: `verify`, `e2e`, `policy`, and
Snyk all passed.
- Confirmed `public-gh/master` is an ancestor of this branch after
fetching `public-gh master`.
- Confirmed `pnpm-lock.yaml` is not included in the branch diff.
- Confirmed migration `0058_wealthy_starbolt.sql` is ordered after
`0057` and uses `IF NOT EXISTS` guards for repeat application.
- Greptile inline review threads are resolved.
## Risks
- Medium risk: this touches heartbeat execution, liveness recovery,
activity rendering, issue routes, shared contracts, docs, and UI.
- Migration risk is mitigated by additive columns/indexes and idempotent
guards.
- Run-ledger liveness backfill is now asynchronous, so the first ledger
response can briefly show historical missing liveness until the
background backfill completes.
- UI screenshot coverage is not included in this packaging pass;
validation is currently through focused component tests.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4, local tool-use coding agent with terminal, git,
GitHub connector, GitHub CLI, and Paperclip API access.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Screenshot note: no before/after screenshots were captured in this PR
packaging pass; the UI changes are covered by focused component tests
listed above.
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:01:49 -05:00
|
|
|
export const createChildIssueSchema = createIssueSchema
|
|
|
|
|
.omit({
|
|
|
|
|
parentId: true,
|
|
|
|
|
inheritExecutionWorkspaceFromIssueId: true,
|
|
|
|
|
})
|
|
|
|
|
.extend({
|
|
|
|
|
acceptanceCriteria: z.array(z.string().trim().min(1).max(500)).max(20).optional(),
|
|
|
|
|
blockParentUntilDone: z.boolean().optional().default(false),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type CreateChildIssue = z.infer<typeof createChildIssueSchema>;
|
|
|
|
|
|
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
|
|
|
|
[codex] Add structured issue-thread interactions (#4244)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Operators supervise that work through issues, comments, approvals,
and the board UI.
> - Some agent proposals need structured board/user decisions, not
hidden markdown conventions or heavyweight governed approvals.
> - Issue-thread interactions already provide a natural thread-native
surface for proposed tasks and questions.
> - This pull request extends that surface with request confirmations,
richer interaction cards, and agent/plugin/MCP helpers.
> - The benefit is that plan approvals and yes/no decisions become
explicit, auditable, and resumable without losing the single-issue
workflow.
## What Changed
- Added persisted issue-thread interactions for suggested tasks,
structured questions, and request confirmations.
- Added board UI cards for interaction review, selection, question
answers, and accept/reject confirmation flows.
- Added MCP and plugin SDK helpers for creating interaction cards from
agents/plugins.
- Updated agent wake instructions, onboarding assets, Paperclip skill
docs, and public docs to prefer structured confirmations for
issue-scoped decisions.
- Rebased the branch onto `public-gh/master` and renumbered branch
migrations to `0063` and `0064`; the idempotency migration uses `ADD
COLUMN IF NOT EXISTS` for old branch users.
## Verification
- `git diff --check public-gh/master..HEAD`
- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
packages/mcp-server/src/tools.test.ts
packages/shared/src/issue-thread-interactions.test.ts
ui/src/lib/issue-thread-interactions.test.ts
ui/src/lib/issue-chat-messages.test.ts
ui/src/components/IssueThreadInteractionCard.test.tsx
ui/src/components/IssueChatThread.test.tsx
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/issue-thread-interactions-service.test.ts
server/src/services/issue-thread-interactions.test.ts` -> 9 files / 79
tests passed
- `pnpm -r typecheck` -> passed, including `packages/db` migration
numbering check
## Risks
- Medium: this adds a new issue-thread interaction model across
db/shared/server/ui/plugin surfaces.
- Migration risk is reduced by placing this branch after current master
migrations (`0063`, `0064`) and making the idempotency column add
idempotent for users who applied the old branch numbering.
- UI interaction behavior is covered by component tests, but this PR
does not include browser screenshots.
> 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-class coding agent runtime. Exact model ID and
context window are not exposed in this Paperclip run; tool use and local
shell/code execution were enabled.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-21 20:15:11 -05:00
|
|
|
export const issueThreadInteractionStatusSchema = z.enum(ISSUE_THREAD_INTERACTION_STATUSES);
|
|
|
|
|
export const issueThreadInteractionKindSchema = z.enum(ISSUE_THREAD_INTERACTION_KINDS);
|
|
|
|
|
export const issueThreadInteractionContinuationPolicySchema = z.enum(
|
|
|
|
|
ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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 suggestedTaskDraftSchema = z.object({
|
|
|
|
|
clientKey: z.string().trim().min(1).max(120),
|
|
|
|
|
parentClientKey: z.string().trim().min(1).max(120).nullable().optional(),
|
|
|
|
|
parentId: z.string().uuid().nullable().optional(),
|
|
|
|
|
title: z.string().trim().min(1).max(240),
|
|
|
|
|
description: z.string().trim().max(20000).nullable().optional(),
|
|
|
|
|
priority: z.enum(ISSUE_PRIORITIES).nullable().optional(),
|
|
|
|
|
assigneeAgentId: z.string().uuid().nullable().optional(),
|
|
|
|
|
assigneeUserId: z.string().trim().min(1).nullable().optional(),
|
|
|
|
|
projectId: z.string().uuid().nullable().optional(),
|
|
|
|
|
goalId: z.string().uuid().nullable().optional(),
|
|
|
|
|
billingCode: z.string().trim().max(120).nullable().optional(),
|
|
|
|
|
labels: z.array(z.string().trim().min(1).max(48)).max(20).optional(),
|
|
|
|
|
hiddenInPreview: z.boolean().optional(),
|
|
|
|
|
}).superRefine((value, ctx) => {
|
|
|
|
|
if (value.assigneeAgentId && value.assigneeUserId) {
|
|
|
|
|
ctx.addIssue({
|
|
|
|
|
code: z.ZodIssueCode.custom,
|
|
|
|
|
message: "Suggested tasks can only target one assignee",
|
|
|
|
|
path: ["assigneeAgentId"],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const suggestTasksPayloadSchema = z.object({
|
|
|
|
|
version: z.literal(1),
|
|
|
|
|
defaultParentId: z.string().uuid().nullable().optional(),
|
|
|
|
|
tasks: z.array(suggestedTaskDraftSchema).min(1).max(50),
|
|
|
|
|
}).superRefine((value, ctx) => {
|
|
|
|
|
const seenClientKeys = new Set<string>();
|
|
|
|
|
for (const [index, task] of value.tasks.entries()) {
|
|
|
|
|
if (seenClientKeys.has(task.clientKey)) {
|
|
|
|
|
ctx.addIssue({
|
|
|
|
|
code: z.ZodIssueCode.custom,
|
|
|
|
|
message: "clientKey must be unique within one interaction",
|
|
|
|
|
path: ["tasks", index, "clientKey"],
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
seenClientKeys.add(task.clientKey);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const suggestTasksResultCreatedTaskSchema = z.object({
|
|
|
|
|
clientKey: z.string().trim().min(1).max(120),
|
|
|
|
|
issueId: z.string().uuid(),
|
|
|
|
|
identifier: z.string().trim().min(1).nullable().optional(),
|
|
|
|
|
title: z.string().trim().min(1).nullable().optional(),
|
|
|
|
|
parentIssueId: z.string().uuid().nullable().optional(),
|
|
|
|
|
parentIdentifier: z.string().trim().min(1).nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const suggestTasksResultSchema = z.object({
|
|
|
|
|
version: z.literal(1),
|
|
|
|
|
createdTasks: z.array(suggestTasksResultCreatedTaskSchema).max(50).optional(),
|
|
|
|
|
skippedClientKeys: z.array(z.string().trim().min(1).max(120)).max(50).optional(),
|
|
|
|
|
rejectionReason: z.string().trim().max(4000).nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const askUserQuestionsQuestionOptionSchema = z.object({
|
|
|
|
|
id: z.string().trim().min(1).max(120),
|
|
|
|
|
label: z.string().trim().min(1).max(120),
|
|
|
|
|
description: z.string().trim().max(500).nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const askUserQuestionsQuestionSchema = z.object({
|
|
|
|
|
id: z.string().trim().min(1).max(120),
|
|
|
|
|
prompt: z.string().trim().min(1).max(500),
|
|
|
|
|
helpText: z.string().trim().max(1000).nullable().optional(),
|
|
|
|
|
selectionMode: z.enum(["single", "multi"]),
|
|
|
|
|
required: z.boolean().optional(),
|
|
|
|
|
options: z.array(askUserQuestionsQuestionOptionSchema).min(1).max(10),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const askUserQuestionsPayloadSchema = z.object({
|
|
|
|
|
version: z.literal(1),
|
|
|
|
|
title: z.string().trim().max(240).nullable().optional(),
|
|
|
|
|
submitLabel: z.string().trim().max(120).nullable().optional(),
|
|
|
|
|
questions: z.array(askUserQuestionsQuestionSchema).min(1).max(10),
|
|
|
|
|
}).superRefine((value, ctx) => {
|
|
|
|
|
const seenQuestionIds = new Set<string>();
|
|
|
|
|
for (const [questionIndex, question] of value.questions.entries()) {
|
|
|
|
|
if (seenQuestionIds.has(question.id)) {
|
|
|
|
|
ctx.addIssue({
|
|
|
|
|
code: z.ZodIssueCode.custom,
|
|
|
|
|
message: "Question ids must be unique within one interaction",
|
|
|
|
|
path: ["questions", questionIndex, "id"],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
seenQuestionIds.add(question.id);
|
|
|
|
|
|
|
|
|
|
const seenOptionIds = new Set<string>();
|
|
|
|
|
for (const [optionIndex, option] of question.options.entries()) {
|
|
|
|
|
if (seenOptionIds.has(option.id)) {
|
|
|
|
|
ctx.addIssue({
|
|
|
|
|
code: z.ZodIssueCode.custom,
|
|
|
|
|
message: "Option ids must be unique within one question",
|
|
|
|
|
path: ["questions", questionIndex, "options", optionIndex, "id"],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
seenOptionIds.add(option.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const askUserQuestionsAnswerSchema = z.object({
|
|
|
|
|
questionId: z.string().trim().min(1).max(120),
|
|
|
|
|
optionIds: z.array(z.string().trim().min(1).max(120)).max(20),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const askUserQuestionsResultSchema = z.object({
|
|
|
|
|
version: z.literal(1),
|
|
|
|
|
answers: z.array(askUserQuestionsAnswerSchema).max(20),
|
|
|
|
|
summaryMarkdown: z.string().max(20000).nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const requestConfirmationHrefSchema = z.string().trim().min(1).max(2000).refine((value) => {
|
|
|
|
|
const lower = value.toLowerCase();
|
|
|
|
|
return !lower.startsWith("javascript:")
|
|
|
|
|
&& !lower.startsWith("data:")
|
|
|
|
|
&& !value.startsWith("//");
|
|
|
|
|
}, "href must not use javascript:, data:, or protocol-relative URLs");
|
|
|
|
|
|
|
|
|
|
const requestConfirmationTargetBaseSchema = z.object({
|
|
|
|
|
label: z.string().trim().min(1).max(120).nullable().optional(),
|
|
|
|
|
href: requestConfirmationHrefSchema.nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const requestConfirmationIssueDocumentTargetSchema = requestConfirmationTargetBaseSchema.extend({
|
|
|
|
|
type: z.literal("issue_document"),
|
|
|
|
|
issueId: z.string().uuid().nullable().optional(),
|
|
|
|
|
documentId: z.string().uuid().nullable().optional(),
|
|
|
|
|
key: issueDocumentKeySchema,
|
|
|
|
|
revisionId: z.string().uuid(),
|
|
|
|
|
revisionNumber: z.number().int().positive().nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const requestConfirmationCustomTargetSchema = requestConfirmationTargetBaseSchema.extend({
|
|
|
|
|
type: z.literal("custom"),
|
|
|
|
|
key: z.string().trim().min(1).max(120),
|
|
|
|
|
revisionId: z.string().trim().min(1).max(255).nullable().optional(),
|
|
|
|
|
revisionNumber: z.number().int().positive().nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const requestConfirmationTargetSchema = z.discriminatedUnion("type", [
|
|
|
|
|
requestConfirmationIssueDocumentTargetSchema,
|
|
|
|
|
requestConfirmationCustomTargetSchema,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export const requestConfirmationPayloadSchema = z.object({
|
|
|
|
|
version: z.literal(1),
|
|
|
|
|
prompt: z.string().trim().min(1).max(1000),
|
|
|
|
|
acceptLabel: z.string().trim().min(1).max(80).nullable().optional(),
|
|
|
|
|
rejectLabel: z.string().trim().min(1).max(80).nullable().optional(),
|
|
|
|
|
rejectRequiresReason: z.boolean().optional(),
|
|
|
|
|
rejectReasonLabel: z.string().trim().min(1).max(160).nullable().optional(),
|
|
|
|
|
allowDeclineReason: z.boolean().optional().default(true),
|
|
|
|
|
declineReasonPlaceholder: z.string().trim().min(1).max(240).nullable().optional(),
|
|
|
|
|
detailsMarkdown: z.string().max(20000).nullable().optional(),
|
|
|
|
|
supersedeOnUserComment: z.boolean().optional(),
|
|
|
|
|
target: requestConfirmationTargetSchema.nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const requestConfirmationResultSchema = z.object({
|
|
|
|
|
version: z.literal(1),
|
|
|
|
|
outcome: z.enum(["accepted", "rejected", "superseded_by_comment", "stale_target"]),
|
|
|
|
|
reason: z.string().trim().max(4000).nullable().optional(),
|
|
|
|
|
commentId: z.string().uuid().nullable().optional(),
|
|
|
|
|
staleTarget: requestConfirmationTargetSchema.nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [
|
|
|
|
|
z.object({
|
|
|
|
|
kind: z.literal("suggest_tasks"),
|
|
|
|
|
idempotencyKey: z.string().trim().max(255).nullable().optional(),
|
|
|
|
|
sourceCommentId: z.string().uuid().nullable().optional(),
|
|
|
|
|
sourceRunId: z.string().uuid().nullable().optional(),
|
|
|
|
|
title: z.string().trim().max(240).nullable().optional(),
|
|
|
|
|
summary: z.string().trim().max(1000).nullable().optional(),
|
|
|
|
|
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
|
|
|
|
|
payload: suggestTasksPayloadSchema,
|
|
|
|
|
}),
|
|
|
|
|
z.object({
|
|
|
|
|
kind: z.literal("ask_user_questions"),
|
|
|
|
|
idempotencyKey: z.string().trim().max(255).nullable().optional(),
|
|
|
|
|
sourceCommentId: z.string().uuid().nullable().optional(),
|
|
|
|
|
sourceRunId: z.string().uuid().nullable().optional(),
|
|
|
|
|
title: z.string().trim().max(240).nullable().optional(),
|
|
|
|
|
summary: z.string().trim().max(1000).nullable().optional(),
|
|
|
|
|
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
|
|
|
|
|
payload: askUserQuestionsPayloadSchema,
|
|
|
|
|
}),
|
|
|
|
|
z.object({
|
|
|
|
|
kind: z.literal("request_confirmation"),
|
|
|
|
|
idempotencyKey: z.string().trim().max(255).nullable().optional(),
|
|
|
|
|
sourceCommentId: z.string().uuid().nullable().optional(),
|
|
|
|
|
sourceRunId: z.string().uuid().nullable().optional(),
|
|
|
|
|
title: z.string().trim().max(240).nullable().optional(),
|
|
|
|
|
summary: z.string().trim().max(1000).nullable().optional(),
|
|
|
|
|
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("none"),
|
|
|
|
|
payload: requestConfirmationPayloadSchema,
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export type CreateIssueThreadInteraction = z.infer<typeof createIssueThreadInteractionSchema>;
|
|
|
|
|
|
|
|
|
|
export const acceptIssueThreadInteractionSchema = z.object({
|
|
|
|
|
selectedClientKeys: z.array(z.string().trim().min(1).max(120)).min(1).max(50).optional(),
|
|
|
|
|
}).superRefine((value, ctx) => {
|
|
|
|
|
const seenClientKeys = new Set<string>();
|
|
|
|
|
for (const [index, clientKey] of (value.selectedClientKeys ?? []).entries()) {
|
|
|
|
|
if (seenClientKeys.has(clientKey)) {
|
|
|
|
|
ctx.addIssue({
|
|
|
|
|
code: z.ZodIssueCode.custom,
|
|
|
|
|
message: "selectedClientKeys must be unique",
|
|
|
|
|
path: ["selectedClientKeys", index],
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
seenClientKeys.add(clientKey);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
export type AcceptIssueThreadInteraction = z.infer<typeof acceptIssueThreadInteractionSchema>;
|
|
|
|
|
|
|
|
|
|
export const rejectIssueThreadInteractionSchema = z.object({
|
|
|
|
|
reason: z.string().trim().max(4000).optional(),
|
|
|
|
|
});
|
|
|
|
|
export type RejectIssueThreadInteraction = z.infer<typeof rejectIssueThreadInteractionSchema>;
|
|
|
|
|
|
|
|
|
|
export const respondIssueThreadInteractionSchema = z.object({
|
|
|
|
|
answers: z.array(askUserQuestionsAnswerSchema).max(20),
|
|
|
|
|
summaryMarkdown: z.string().max(20000).nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
export type RespondIssueThreadInteraction = z.infer<typeof respondIssueThreadInteractionSchema>;
|
|
|
|
|
|
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 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>;
|