mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Make workspace, agent, and schedule automation consistent (#2186)
* feat(workspaces): align agent and schedule automation Make workspace identity the shared placement boundary for MCP and CLI, while caller identity determines parentage. Keep heartbeats minimal and cron-based without changing legacy rolling interval semantics. * test(cli): expect canonical schedule cadence * fix(automation): preserve workspace and schedule compatibility * fix(automation): preserve compatibility edges * fix(workspaces): align MCP lifecycle resolution * fix(workspaces): preserve creation intent * fix(workspaces): preserve branch and schedule identity
This commit is contained in:
@@ -981,7 +981,7 @@ const AgentAttachmentsSchema = z.unknown().transform(normalizeAgentAttachments).
|
||||
|
||||
export const ChangeRequestCheckoutSourceSchema = z.object({
|
||||
kind: z.literal("change_request"),
|
||||
forge: z.string().optional().default("github"),
|
||||
forge: z.string().optional(),
|
||||
number: z.number().int().positive(),
|
||||
projectPath: z.string().optional(),
|
||||
});
|
||||
@@ -1249,6 +1249,9 @@ export const CreateAgentRequestMessageSchema = z.object({
|
||||
config: AgentSessionConfigSchema,
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
// Optional caller context lets managed CLI invocations use the same daemon-owned
|
||||
// workspace and parentage policy as agent-scoped MCP creation.
|
||||
callerAgentId: z.string().optional(),
|
||||
worktreeName: z.string().optional(),
|
||||
initialPrompt: z.string().optional(),
|
||||
clientMessageId: z.string().optional(),
|
||||
@@ -2061,9 +2064,11 @@ export const WorkspaceCreateRequestSchema = z.object({
|
||||
cwd: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
action: z.enum(["branch-off", "checkout"]).optional(),
|
||||
// Target branch name for checkout, or new branch name for branch-off.
|
||||
// Target branch for checkout, or base ref for branch-off.
|
||||
refName: z.string().min(1).optional(),
|
||||
baseBranch: z.string().optional(),
|
||||
// New branch name for branch-off. The worktree path may use a different slug.
|
||||
branchName: z.string().min(1).optional(),
|
||||
checkoutSource: ChangeRequestCheckoutSourceSchema.optional(),
|
||||
// COMPAT(githubPrNumber): added in v0.1.106, remove after 2026-12-28 once
|
||||
// clients send checkoutSource.
|
||||
|
||||
@@ -1031,6 +1031,25 @@ describe("workspace message schemas", () => {
|
||||
expect(newWorktree.type).toBe("workspace.create.request");
|
||||
expect(newWorktree.source.kind).toBe("worktree");
|
||||
|
||||
const branchOff = WorkspaceCreateRequestSchema.parse({
|
||||
type: "workspace.create.request",
|
||||
requestId: "req-branch-off",
|
||||
source: {
|
||||
kind: "worktree",
|
||||
cwd: "/tmp/repo",
|
||||
action: "branch-off",
|
||||
branchName: "feature/auth",
|
||||
worktreeSlug: "feature-auth",
|
||||
},
|
||||
});
|
||||
expect(branchOff.source).toEqual({
|
||||
kind: "worktree",
|
||||
cwd: "/tmp/repo",
|
||||
action: "branch-off",
|
||||
branchName: "feature/auth",
|
||||
worktreeSlug: "feature-auth",
|
||||
});
|
||||
|
||||
// Directory source must also be accepted.
|
||||
const newDirectory = WorkspaceCreateRequestSchema.parse({
|
||||
type: "workspace.create.request",
|
||||
|
||||
21
packages/protocol/src/schedule/cadence.test.ts
Normal file
21
packages/protocol/src/schedule/cadence.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { everyMsToFiveFieldCron } from "./cadence.js";
|
||||
|
||||
describe("everyMsToFiveFieldCron", () => {
|
||||
it.each([
|
||||
[60_000, "*/1 * * * *"],
|
||||
[15 * 60_000, "*/15 * * * *"],
|
||||
[60 * 60_000, "0 * * * *"],
|
||||
[6 * 60 * 60_000, "0 */6 * * *"],
|
||||
[24 * 60 * 60_000, "0 0 * * *"],
|
||||
])("converts %i milliseconds", (everyMs, cron) => {
|
||||
expect(everyMsToFiveFieldCron(everyMs)).toBe(cron);
|
||||
});
|
||||
|
||||
it.each([30_000, 7 * 60_000, 5 * 60 * 60_000, 48 * 60 * 60_000])(
|
||||
"rejects unrepresentable interval %i",
|
||||
(everyMs) => {
|
||||
expect(everyMsToFiveFieldCron(everyMs)).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
27
packages/protocol/src/schedule/cadence.ts
Normal file
27
packages/protocol/src/schedule/cadence.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Convert an exact rolling interval to an equivalent five-field cron cadence.
|
||||
* Returns null when cron's calendar boundaries would change the interval.
|
||||
*/
|
||||
export function everyMsToFiveFieldCron(everyMs: number): string | null {
|
||||
const minutes = everyMs / 60_000;
|
||||
if (!Number.isInteger(minutes) || minutes <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (minutes < 60 && 60 % minutes === 0) {
|
||||
return `*/${minutes} * * * *`;
|
||||
}
|
||||
if (minutes === 60) {
|
||||
return "0 * * * *";
|
||||
}
|
||||
if (minutes % 60 !== 0) {
|
||||
return null;
|
||||
}
|
||||
const hours = minutes / 60;
|
||||
if (hours < 24 && 24 % hours === 0) {
|
||||
return `0 */${hours} * * *`;
|
||||
}
|
||||
if (hours === 24) {
|
||||
return "0 0 * * *";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user