Merge pull request #1575 from Runfusion/feature/workflow-owned-merge-s02-merge-request-projection
refactor(workflow): S02 merge request projection onto work items
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: "S02: merge request projection onto work items"
|
||||||
|
type: refactor
|
||||||
|
status: draft-stack-handoff
|
||||||
|
date: 2026-06-09
|
||||||
|
slice: S02
|
||||||
|
milestone: "Foundation"
|
||||||
|
origin: docs/plans/2026-06-09-003-refactor-workflow-owned-merge-full-migration-slices-plan.md
|
||||||
|
stack_base: feature/workflow-owned-merge-retry-scheduling-plan
|
||||||
|
---
|
||||||
|
|
||||||
|
# S02: merge request projection onto work items
|
||||||
|
|
||||||
|
## Stack Role
|
||||||
|
|
||||||
|
This draft PR reserves the S02 review slot in the workflow-owned merge,
|
||||||
|
retry, scheduling, and recovery migration stack. It is intentionally a handoff
|
||||||
|
artifact, not the completed implementation for this slice.
|
||||||
|
|
||||||
|
## Milestone
|
||||||
|
|
||||||
|
Foundation
|
||||||
|
|
||||||
|
## Depends On
|
||||||
|
|
||||||
|
S1 workflow work-item schema and store API.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Project existing merge request records into workflow work-item state so dashboards and schedulers can dual-read before cutover.
|
||||||
|
|
||||||
|
## Expected File Scope
|
||||||
|
|
||||||
|
packages/core/src/store.ts; packages/core/src/task-merge.ts; packages/core/src/types.ts; merge-request and dual-observe tests.
|
||||||
|
|
||||||
|
## Expected Tests
|
||||||
|
|
||||||
|
Projection tests for queued/running/retrying/manual-required/succeeded/exhausted states, hard cancel cancellation, and restart idempotency.
|
||||||
|
|
||||||
|
## Exit Gate
|
||||||
|
|
||||||
|
Every merge request state has a lossless workflow work-item equivalent.
|
||||||
|
|
||||||
|
## Full Plan
|
||||||
|
|
||||||
|
See `docs/plans/2026-06-09-003-refactor-workflow-owned-merge-full-migration-slices-plan.md`.
|
||||||
@@ -69,6 +69,87 @@ describe("TaskStore merge request record + completion handoff marker", () => {
|
|||||||
expect(store.transitionMergeRequestState(taskId, "succeeded", { now: "2026-05-30T00:00:05.000Z" }).state).toBe("succeeded");
|
expect(store.transitionMergeRequestState(taskId, "succeeded", { now: "2026-05-30T00:00:05.000Z" }).state).toBe("succeeded");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("projects merge request states onto workflow work items", async () => {
|
||||||
|
const cases = [
|
||||||
|
{ mergeState: "queued", workState: "runnable", kind: "merge" },
|
||||||
|
{ mergeState: "running", workState: "running", kind: "merge" },
|
||||||
|
{ mergeState: "retrying", workState: "retrying", kind: "merge" },
|
||||||
|
{ mergeState: "manual-required", workState: "manual-required", kind: "manual-hold" },
|
||||||
|
{ mergeState: "succeeded", workState: "succeeded", kind: "merge" },
|
||||||
|
{ mergeState: "exhausted", workState: "exhausted", kind: "merge" },
|
||||||
|
{ mergeState: "cancelled", workState: "cancelled", kind: "merge" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const { mergeState, workState, kind } of cases) {
|
||||||
|
const taskId = await createTask();
|
||||||
|
store.upsertMergeRequestRecord(taskId, {
|
||||||
|
state: mergeState,
|
||||||
|
attemptCount: 3,
|
||||||
|
lastError: mergeState === "manual-required" ? "needs human" : "last failure",
|
||||||
|
now: "2026-05-30T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = store.projectMergeRequestToWorkflowWorkItem(taskId, {
|
||||||
|
now: "2026-05-30T00:00:01.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(item).toMatchObject({
|
||||||
|
runId: `merge-request:${taskId}`,
|
||||||
|
taskId,
|
||||||
|
nodeId: "builtin.merge.request",
|
||||||
|
kind,
|
||||||
|
state: workState,
|
||||||
|
attempt: 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects merge requests idempotently across restart-style replays", async () => {
|
||||||
|
const taskId = await createTask();
|
||||||
|
store.upsertMergeRequestRecord(taskId, {
|
||||||
|
state: "retrying",
|
||||||
|
attemptCount: 2,
|
||||||
|
lastError: "network reset",
|
||||||
|
now: "2026-05-30T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = store.projectMergeRequestToWorkflowWorkItem(taskId, { now: "2026-05-30T00:00:01.000Z" });
|
||||||
|
const second = store.projectMergeRequestToWorkflowWorkItem(taskId, { now: "2026-05-30T00:00:02.000Z" });
|
||||||
|
|
||||||
|
expect(second?.id).toBe(first?.id);
|
||||||
|
expect(store.listWorkflowWorkItemsForTask(taskId, { kinds: ["merge"] })).toHaveLength(1);
|
||||||
|
expect(second).toMatchObject({ state: "retrying", attempt: 2, lastError: "network reset" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels stale manual-hold projection when the same merge request succeeds", async () => {
|
||||||
|
const taskId = await createTask();
|
||||||
|
store.upsertMergeRequestRecord(taskId, {
|
||||||
|
state: "manual-required",
|
||||||
|
attemptCount: 1,
|
||||||
|
lastError: "needs human",
|
||||||
|
now: "2026-05-30T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hold = store.projectMergeRequestToWorkflowWorkItem(taskId, { now: "2026-05-30T00:00:01.000Z" });
|
||||||
|
store.upsertMergeRequestRecord(taskId, {
|
||||||
|
state: "succeeded",
|
||||||
|
attemptCount: 1,
|
||||||
|
lastError: null,
|
||||||
|
now: "2026-05-30T00:00:02.000Z",
|
||||||
|
});
|
||||||
|
const merge = store.projectMergeRequestToWorkflowWorkItem(taskId, { now: "2026-05-30T00:00:03.000Z" });
|
||||||
|
|
||||||
|
expect(merge).toMatchObject({ kind: "merge", state: "succeeded" });
|
||||||
|
expect(store.getWorkflowWorkItem(hold?.id ?? "")).toMatchObject({
|
||||||
|
kind: "manual-hold",
|
||||||
|
state: "cancelled",
|
||||||
|
lastError: "superseded-by-merge-request-projection",
|
||||||
|
});
|
||||||
|
expect(store.listWorkflowWorkItemsForTask(taskId).filter((item) => item.state !== "cancelled")).toEqual([
|
||||||
|
expect.objectContaining({ id: merge?.id, kind: "merge", state: "succeeded" }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects invalid merge-request transitions", async () => {
|
it("rejects invalid merge-request transitions", async () => {
|
||||||
const taskId = await createTask();
|
const taskId = await createTask();
|
||||||
store.upsertMergeRequestRecord(taskId, { state: "queued" });
|
store.upsertMergeRequestRecord(taskId, { state: "queued" });
|
||||||
@@ -112,4 +193,33 @@ describe("TaskStore merge request record + completion handoff marker", () => {
|
|||||||
expect(store.getMergeRequestRecord(taskId)?.state).toBe("cancelled");
|
expect(store.getMergeRequestRecord(taskId)?.state).toBe("cancelled");
|
||||||
expect(store.getCompletionHandoffAcceptedMarker(taskId)).toBeNull();
|
expect(store.getCompletionHandoffAcceptedMarker(taskId)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("cancels active workflow merge work on user hard-cancel from in-review to todo", async () => {
|
||||||
|
const taskId = await createTask();
|
||||||
|
await store.moveTask(taskId, "todo");
|
||||||
|
await store.moveTask(taskId, "in-progress");
|
||||||
|
await store.handoffToReview(taskId, {
|
||||||
|
ownerAgentId: "agent-test",
|
||||||
|
evidence: { reason: "fn_task_done", runId: "run-1", agentId: "agent-test" },
|
||||||
|
});
|
||||||
|
store.setCompletionHandoffAcceptedMarker(taskId, { source: "executor:fn_task_done" });
|
||||||
|
const mergeWork = store.upsertWorkflowWorkItem({
|
||||||
|
runId: "run-merge",
|
||||||
|
taskId,
|
||||||
|
nodeId: "builtin.merge.request",
|
||||||
|
kind: "merge",
|
||||||
|
state: "running",
|
||||||
|
leaseOwner: "worker-a",
|
||||||
|
leaseExpiresAt: "2026-05-30T00:05:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.moveTask(taskId, "todo", { moveSource: "user" });
|
||||||
|
|
||||||
|
expect(store.getWorkflowWorkItem(mergeWork.id)).toMatchObject({
|
||||||
|
state: "cancelled",
|
||||||
|
leaseOwner: null,
|
||||||
|
leaseExpiresAt: null,
|
||||||
|
lastError: "cancelled-by-user-hard-cancel",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, isLocale, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION, sanitizeCliAgentSettings, sanitizeCliAgentsSettings, CLI_AGENT_ADAPTER_IDS, CLI_AGENT_AUTONOMY_MODES } from "./types.js";
|
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, isLocale, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION, sanitizeCliAgentSettings, sanitizeCliAgentsSettings, CLI_AGENT_ADAPTER_IDS, CLI_AGENT_AUTONOMY_MODES } from "./types.js";
|
||||||
export type { Column, ColumnId, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrConflictState, PrConflictDiagnostics, PrCheckState, PrCheckStatus, PrStatus, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, BranchGroupPrState, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeIntegrationWorktreeMode, MergeAdvanceAutoSyncMode, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, MergerMode, MergerSettings, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, SecretsEnvConfig, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, Locale, ExecutionMode, TaskPriority, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, MergeRequestState, MergeRequestRecord, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, HandoffEvidence, HandoffToReviewOptions, UnavailableNodePolicy, OwningNodeHandoffPolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, SandboxProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, CheckoutClaimPrecondition, TaskClaimRow, CentralClaimStore, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, WorktrunkSettings, WorktrunkOnFailure, TaskBranchContext, CliAgentSettings } from "./types.js";
|
export type { Column, ColumnId, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrConflictState, PrConflictDiagnostics, PrCheckState, PrCheckStatus, PrStatus, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, BranchGroupPrState, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeIntegrationWorktreeMode, MergeAdvanceAutoSyncMode, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, MergerMode, MergerSettings, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, SecretsEnvConfig, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, Locale, ExecutionMode, TaskPriority, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, MergeRequestState, MergeRequestRecord, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, HandoffEvidence, HandoffToReviewOptions, UnavailableNodePolicy, OwningNodeHandoffPolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, SandboxProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, CheckoutClaimPrecondition, TaskClaimRow, CentralClaimStore, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, WorktrunkSettings, WorktrunkOnFailure, TaskBranchContext, CliAgentSettings } from "./types.js";
|
||||||
export { AGENT_VALID_TRANSITIONS, DUPLICATE_OF_METADATA_KEY } from "./types.js";
|
export { AGENT_VALID_TRANSITIONS, DUPLICATE_OF_METADATA_KEY } from "./types.js";
|
||||||
export {
|
export {
|
||||||
resolveEntryPointBranchAssignment,
|
resolveEntryPointBranchAssignment,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { mkdir, readdir, readFile, writeFile, rename, unlink } from "node:fs/promises";
|
import { mkdir, readdir, readFile, writeFile, rename, unlink } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { existsSync, watch, type FSWatcher } from "node:fs";
|
import { existsSync, watch, type FSWatcher } from "node:fs";
|
||||||
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, ColumnId, CheckoutClaimPrecondition, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority, SourceType, WorkflowStepTemplate, Agent, AutostashOrphanRecord, TaskCommitAssociation, TaskCommitAssociationMatchSource, TaskCommitAssociationConfidence, GithubIssueAction, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, HandoffToReviewOptions, GoalCitation, GoalCitationFilter, GoalCitationInput, GoalCitationSurface, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, TaskBranchAssignmentMode, MergeRequestRecord, MergeRequestState, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, PrEntity, PrEntityCreateInput, PrEntityUpdate, PrEntityState, PrThreadState, PrThreadOutcome, PrConflictState, PrChecksRollup, PrReviewDecision } from "./types.js";
|
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, ColumnId, CheckoutClaimPrecondition, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority, SourceType, WorkflowStepTemplate, Agent, AutostashOrphanRecord, TaskCommitAssociation, TaskCommitAssociationMatchSource, TaskCommitAssociationConfidence, GithubIssueAction, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, HandoffToReviewOptions, GoalCitation, GoalCitationFilter, GoalCitationInput, GoalCitationSurface, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, TaskBranchAssignmentMode, MergeRequestRecord, MergeRequestState, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, PrEntity, PrEntityCreateInput, PrEntityUpdate, PrEntityState, PrThreadState, PrThreadOutcome, PrConflictState, PrChecksRollup, PrReviewDecision } from "./types.js";
|
||||||
import { createActivityLogSnapshot, createRunAuditSnapshot, createTaskMetadataSnapshot, toTaskMetadataRecord, validateSnapshotEnvelope, type ActivityLogSnapshot, type RunAuditSnapshot, type TaskMetadataSnapshot } from "./shared-mesh-state.js";
|
import { createActivityLogSnapshot, createRunAuditSnapshot, createTaskMetadataSnapshot, toTaskMetadataRecord, validateSnapshotEnvelope, type ActivityLogSnapshot, type RunAuditSnapshot, type TaskMetadataSnapshot } from "./shared-mesh-state.js";
|
||||||
import { VALID_TRANSITIONS, COLUMNS, DEFAULT_SETTINGS, isGlobalOnlySettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey } from "./types.js";
|
import { VALID_TRANSITIONS, COLUMNS, DEFAULT_SETTINGS, isGlobalOnlySettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey } from "./types.js";
|
||||||
import { DEFAULT_PROJECT_SETTINGS } from "./settings-schema.js";
|
import { DEFAULT_PROJECT_SETTINGS } from "./settings-schema.js";
|
||||||
@@ -7149,6 +7149,11 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.cancelActiveWorkflowWorkItemsForTask(id, {
|
||||||
|
kinds: ["merge", "manual-hold"],
|
||||||
|
now: movedAt,
|
||||||
|
lastError: "cancelled-by-user-hard-cancel",
|
||||||
|
});
|
||||||
this.clearCompletionHandoffAcceptedMarker(id);
|
this.clearCompletionHandoffAcceptedMarker(id);
|
||||||
}
|
}
|
||||||
if (toColumn === "done") {
|
if (toColumn === "done") {
|
||||||
@@ -8854,6 +8859,19 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
|||||||
return state === "succeeded" || state === "failed" || state === "cancelled" || state === "exhausted";
|
return state === "succeeded" || state === "failed" || state === "cancelled" || state === "exhausted";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private workflowStateForMergeRequestState(state: MergeRequestState): WorkflowWorkItemState {
|
||||||
|
const states: Record<MergeRequestState, WorkflowWorkItemState> = {
|
||||||
|
queued: "runnable",
|
||||||
|
running: "running",
|
||||||
|
retrying: "retrying",
|
||||||
|
succeeded: "succeeded",
|
||||||
|
exhausted: "exhausted",
|
||||||
|
cancelled: "cancelled",
|
||||||
|
"manual-required": "manual-required",
|
||||||
|
};
|
||||||
|
return states[state];
|
||||||
|
}
|
||||||
|
|
||||||
private rowToWorkflowWorkItem(row: WorkflowWorkItemRow): WorkflowWorkItem {
|
private rowToWorkflowWorkItem(row: WorkflowWorkItemRow): WorkflowWorkItem {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -8962,6 +8980,43 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
|||||||
return row ? this.rowToMergeRequestRecord(row) : null;
|
return row ? this.rowToMergeRequestRecord(row) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectMergeRequestToWorkflowWorkItem(
|
||||||
|
taskId: string,
|
||||||
|
opts: MergeRequestWorkflowProjectionOptions = {},
|
||||||
|
): WorkflowWorkItem | null {
|
||||||
|
return this.db.transactionImmediate(() => {
|
||||||
|
const record = this.getMergeRequestRecord(taskId);
|
||||||
|
if (!record) return null;
|
||||||
|
const state = this.workflowStateForMergeRequestState(record.state);
|
||||||
|
const kind = record.state === "manual-required" ? "manual-hold" : "merge";
|
||||||
|
const item = this.upsertWorkflowWorkItem({
|
||||||
|
runId: opts.runId ?? `merge-request:${taskId}`,
|
||||||
|
taskId,
|
||||||
|
nodeId: opts.nodeId ?? "builtin.merge.request",
|
||||||
|
kind,
|
||||||
|
state,
|
||||||
|
attempt: record.attemptCount,
|
||||||
|
lastError: record.lastError,
|
||||||
|
blockedReason: record.state === "manual-required" ? record.lastError ?? "manual merge required" : null,
|
||||||
|
now: opts.now ?? record.updatedAt,
|
||||||
|
});
|
||||||
|
this.cancelActiveWorkflowWorkItemsForTask(taskId, {
|
||||||
|
kinds: [kind === "manual-hold" ? "merge" : "manual-hold"],
|
||||||
|
now: opts.now ?? record.updatedAt,
|
||||||
|
lastError: "superseded-by-merge-request-projection",
|
||||||
|
});
|
||||||
|
this.insertRunAuditEventRow({
|
||||||
|
taskId,
|
||||||
|
runId: item.runId,
|
||||||
|
domain: "database",
|
||||||
|
mutationType: "mergeRequest:workflow-projection",
|
||||||
|
target: item.id,
|
||||||
|
metadata: { taskId, mergeRequestState: record.state, workflowState: item.state, workItemKind: item.kind },
|
||||||
|
});
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
upsertWorkflowWorkItem(input: WorkflowWorkItemUpsertInput): WorkflowWorkItem {
|
upsertWorkflowWorkItem(input: WorkflowWorkItemUpsertInput): WorkflowWorkItem {
|
||||||
return this.db.transactionImmediate(() => {
|
return this.db.transactionImmediate(() => {
|
||||||
const existing = this.db
|
const existing = this.db
|
||||||
@@ -9083,6 +9138,42 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
|||||||
return row ? this.rowToWorkflowWorkItem(row) : null;
|
return row ? this.rowToWorkflowWorkItem(row) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listWorkflowWorkItemsForTask(taskId: string, opts: { kinds?: WorkflowWorkItemKind[] } = {}): WorkflowWorkItem[] {
|
||||||
|
const conditions = ["taskId = ?"];
|
||||||
|
const params: unknown[] = [taskId];
|
||||||
|
if (opts.kinds?.length) {
|
||||||
|
conditions.push(`kind IN (${opts.kinds.map(() => "?").join(", ")})`);
|
||||||
|
params.push(...opts.kinds);
|
||||||
|
}
|
||||||
|
const rows = this.db
|
||||||
|
.prepare(
|
||||||
|
`SELECT *
|
||||||
|
FROM workflow_work_items
|
||||||
|
WHERE ${conditions.join(" AND ")}
|
||||||
|
ORDER BY createdAt ASC, id ASC`,
|
||||||
|
)
|
||||||
|
.all(...params) as WorkflowWorkItemRow[];
|
||||||
|
return rows.map((row) => this.rowToWorkflowWorkItem(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelActiveWorkflowWorkItemsForTask(
|
||||||
|
taskId: string,
|
||||||
|
opts: { kinds?: WorkflowWorkItemKind[]; now?: string; lastError?: string | null } = {},
|
||||||
|
): WorkflowWorkItem[] {
|
||||||
|
return this.db.transactionImmediate(() => {
|
||||||
|
const activeStates: WorkflowWorkItemState[] = ["runnable", "running", "held", "retrying", "manual-required"];
|
||||||
|
const items = this.listWorkflowWorkItemsForTask(taskId, opts).filter((item) => activeStates.includes(item.state));
|
||||||
|
return items.map((item) =>
|
||||||
|
this.transitionWorkflowWorkItem(item.id, "cancelled", {
|
||||||
|
now: opts.now,
|
||||||
|
leaseOwner: null,
|
||||||
|
leaseExpiresAt: null,
|
||||||
|
lastError: opts.lastError ?? item.lastError ?? "cancelled-by-user-hard-cancel",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
listDueWorkflowWorkItems(filter: WorkflowWorkItemDueFilter = {}): WorkflowWorkItem[] {
|
listDueWorkflowWorkItems(filter: WorkflowWorkItemDueFilter = {}): WorkflowWorkItem[] {
|
||||||
const now = filter.now ?? new Date().toISOString();
|
const now = filter.now ?? new Date().toISOString();
|
||||||
const includeExpiredRunning = !filter.states || filter.states.includes("running");
|
const includeExpiredRunning = !filter.states || filter.states.includes("running");
|
||||||
|
|||||||
@@ -160,6 +160,12 @@ export interface WorkflowWorkItemDueFilter {
|
|||||||
states?: WorkflowWorkItemState[];
|
states?: WorkflowWorkItemState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MergeRequestWorkflowProjectionOptions {
|
||||||
|
runId?: string;
|
||||||
|
nodeId?: string;
|
||||||
|
now?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MergeQueueEntry {
|
export interface MergeQueueEntry {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
enqueuedAt: string;
|
enqueuedAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user