paseo-req-kit 0.0.0-stage → 1.7.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -2
  4. package/SCOPE.md +11 -0
  5. package/bin/locate.mjs +46 -0
  6. package/bin/req-doctor.mjs +21 -0
  7. package/bin/req.mjs +175 -0
  8. package/bin/start-pipeline +21 -0
  9. package/bin/start-pipeline.mjs +22 -0
  10. package/config.example.json +101 -0
  11. package/contracts/README.md +18 -0
  12. package/contracts/feature-files.md +87 -0
  13. package/contracts/notifier-event.md +71 -0
  14. package/contracts/result-file.md +17 -0
  15. package/contracts/spec.md +34 -0
  16. package/contracts/state.md +44 -0
  17. package/contracts/verify.md +39 -0
  18. package/docs/adr/0001-step-agents-under-origin-chat.md +15 -0
  19. package/docs/adr/0002-no-directory-switch-in-chat.md +15 -0
  20. package/docs/adr/0003-script-engine-not-hub.md +15 -0
  21. package/docs/adr/0004-result-files.md +15 -0
  22. package/docs/adr/0005-no-default-branch-local-commits-only.md +15 -0
  23. package/docs/adr/0006-skill-install-targets.md +15 -0
  24. package/docs/adr/0007-own-interview-skill.md +15 -0
  25. package/docs/adr/0008-entries-do-not-call-entries.md +15 -0
  26. package/docs/adr/0009-versioned-install-layout.md +15 -0
  27. package/docs/adr/0010-generated-feature-overview.md +15 -0
  28. package/docs/adr/0011-resume-after-completion.md +15 -0
  29. package/docs/adr/0012-review-panel.md +18 -0
  30. package/docs/adr/0013-no-release-tooling.md +15 -0
  31. package/docs/adr/0014-notifier-slot.md +19 -0
  32. package/docs/adr/0015-incremental-continue.md +17 -0
  33. package/docs/adr/0016-upgrade-from-tags.md +19 -0
  34. package/docs/adr/0017-use-paseo-capabilities.md +18 -0
  35. package/docs/adr/0018-own-worktree-option.md +21 -0
  36. package/docs/adr/0019-parallel-orchestration.md +21 -0
  37. package/docs/adr/0020-scheduling-through-the-chat.md +21 -0
  38. package/docs/adr/0021-npm-version-manager.md +19 -0
  39. package/docs/adr/0022-develop-and-verify-phases.md +32 -0
  40. package/docs/adr/README.md +26 -0
  41. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +78 -0
  42. package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +348 -0
  43. package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +85 -0
  44. package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +251 -0
  45. package/engine/prompts/00-shared-context.md +100 -0
  46. package/engine/prompts/02-spec-review.md +46 -0
  47. package/engine/prompts/03-design.md +50 -0
  48. package/engine/prompts/03a-design-proposal.md +30 -0
  49. package/engine/prompts/04-design-review.md +72 -0
  50. package/engine/prompts/04b-design-revise.md +34 -0
  51. package/engine/prompts/05-implement.md +66 -0
  52. package/engine/prompts/06-code-review.md +66 -0
  53. package/engine/prompts/07-review-fix.md +41 -0
  54. package/engine/prompts/07b-handoff.md +45 -0
  55. package/engine/prompts/08-service-start.md +59 -0
  56. package/engine/prompts/09-verify.md +90 -0
  57. package/engine/prompts/10-defect-fix.md +41 -0
  58. package/engine/prompts/10b-reverify.md +43 -0
  59. package/engine/prompts/11-report.md +93 -0
  60. package/engine/prompts/browser-verify.md +13 -0
  61. package/engine/prompts/incremental.md +21 -0
  62. package/engine/prompts/panel-member.md +38 -0
  63. package/engine/prompts/paseo-services.md +9 -0
  64. package/engine/prompts/verify-deployed.md +14 -0
  65. package/engine/src/activity.ts +87 -0
  66. package/engine/src/cli-args.ts +204 -0
  67. package/engine/src/config.ts +479 -0
  68. package/engine/src/configure.ts +237 -0
  69. package/engine/src/control.ts +230 -0
  70. package/engine/src/daemon-check.ts +104 -0
  71. package/engine/src/daemon-version.ts +66 -0
  72. package/engine/src/digest.ts +133 -0
  73. package/engine/src/doctor.ts +435 -0
  74. package/engine/src/eval.ts +324 -0
  75. package/engine/src/features.ts +268 -0
  76. package/engine/src/git.ts +30 -0
  77. package/engine/src/launch.ts +405 -0
  78. package/engine/src/locations.ts +55 -0
  79. package/engine/src/main.ts +313 -0
  80. package/engine/src/messages.ts +308 -0
  81. package/engine/src/notifiers.ts +76 -0
  82. package/engine/src/notify-test.ts +97 -0
  83. package/engine/src/paseo-json.ts +73 -0
  84. package/engine/src/paseo-profiles.ts +133 -0
  85. package/engine/src/paseo-runtime.ts +437 -0
  86. package/engine/src/paths.ts +10 -0
  87. package/engine/src/pipeline.ts +1126 -0
  88. package/engine/src/preflight.ts +298 -0
  89. package/engine/src/prompts.ts +72 -0
  90. package/engine/src/queue.ts +61 -0
  91. package/engine/src/runtime-loader.ts +14 -0
  92. package/engine/src/runtime.ts +155 -0
  93. package/engine/src/schedule-time.ts +51 -0
  94. package/engine/src/smoke-test.ts +202 -0
  95. package/engine/src/spec-review.ts +100 -0
  96. package/engine/src/state.ts +278 -0
  97. package/engine/src/steps.ts +242 -0
  98. package/engine/src/usage.ts +67 -0
  99. package/engine/src/worktree.ts +72 -0
  100. package/install.mjs +928 -0
  101. package/notifiers/im-template.mjs +58 -0
  102. package/package.json +67 -4
  103. package/principles/INDEX.md +11 -0
  104. package/skills/acceptance-criteria/SKILL.md +52 -0
  105. package/skills/principle-delete-before-add/SKILL.md +33 -0
  106. package/skills/principle-dont-block-on-humans/SKILL.md +35 -0
  107. package/skills/principle-fix-root-cause/SKILL.md +33 -0
  108. package/skills/principle-prove-it-works/SKILL.md +33 -0
  109. package/skills/principle-report-honestly/SKILL.md +33 -0
  110. package/skills/principle-speak-to-reader/SKILL.md +33 -0
  111. package/skills/principle-this-requirement-only/SKILL.md +33 -0
  112. package/skills/req/SKILL.md +99 -0
  113. package/skills/req/playbooks/accept.md +65 -0
  114. package/skills/req/playbooks/change.md +51 -0
  115. package/skills/req/playbooks/incident.md +55 -0
  116. package/skills/req/playbooks/new.md +83 -0
  117. package/skills/req/playbooks/small.md +48 -0
  118. package/skills/req/playbooks/status.md +39 -0
  119. package/skills/req/playbooks/takeover.md +51 -0
  120. package/skills/req/playbooks/verify.md +57 -0
  121. package/skills/req-doctor/SKILL.md +36 -0
  122. package/skills/req-retro/SKILL.md +79 -0
  123. package/skills/req-setup/SKILL.md +101 -0
  124. package/skills/req-setup/templates/paseo/copy-local-files.mjs +32 -0
  125. package/skills/req-setup/templates/paseo/with-port.mjs +36 -0
  126. package/skills/requirement-interview/SKILL.md +94 -0
  127. package/skills/requirement-kickoff/SKILL.md +17 -0
  128. package/skills/result-report/SKILL.md +34 -0
  129. package/skills/spec-writing/SKILL.md +41 -0
  130. package/skills/spec-writing/spec-template.md +106 -0
@@ -0,0 +1,437 @@
1
+ import type { PaseoAgentHandle, PaseoClient } from "@getpaseo/client";
2
+ import { summarizeActivity } from "./activity.js";
3
+ import { type AgentProfile, isResolved, type PipelineConfig } from "./config.js";
4
+ import { profilesFromDaemonConfig } from "./paseo-profiles.js";
5
+ import { cronAt, localTimeZone } from "./schedule-time.js";
6
+ import { samePath } from "./paths.js";
7
+ import type { AgentRuntime, ManagedService, ProviderQuota, RuntimeFactory, ServiceTarget, StepAgent, WaitOutcome } from "./runtime.js";
8
+
9
+ // Checked against @getpaseo/client 0.11.2 (packages/client/src/index.ts) and the
10
+ // daemon (packages/server/src/server/session.ts, agent/create-agent/intent.ts):
11
+ // - agents.create({ config: { provider: "provider/model", modeId }, cwd, title, labels, prompt, parent })
12
+ // With `parent`, the daemon places the agent in the parent's workspace AND uses the
13
+ // parent's cwd; the requested cwd is ignored. The pipeline therefore requires the chat's
14
+ // cwd to be the repository (verifyOrigin), and verifies every created agent's cwd.
15
+ // SDK-created subagents do not post finish notifications to the parent.
16
+ // - handle.waitForFinish(timeoutMs) -> { status: idle|error|permission|timeout, final, error, lastMessage }
17
+ // The daemon answers immediately when the agent is already idle or has a pending permission.
18
+ // - handle.send(text, { activeTurnBehavior }) -> the daemon interrupts a running turn
19
+ // unless activeTurnBehavior is "steer".
20
+ // - config.get() -> { config: { agentProfiles?: AgentProfile[], mcp, browserTools, ... } }
21
+ // - providers.listUsage() -> { providers: [{ providerId, displayName, windows: [{ label, usedPct, resetsAt, tone }] }] }
22
+ // - agent.timeline.refetch({ direction: "tail", limit }) -> { entries: [{ timestamp, item }] }
23
+ // - Workspace scripts (paseo.json) are not on the public client in 0.11; they are reached
24
+ // through the exported internal DaemonClient (listWorkspaceScripts, startWorkspaceScriptWithStatus,
25
+ // stopWorkspaceScript). See WorkspaceScripts below; every use degrades to "not available".
26
+
27
+ export type SdkClient = Pick<PaseoClient, "agents" | "close"> &
28
+ Partial<Pick<PaseoClient, "config" | "providers" | "terminals" | "workspaces">>;
29
+
30
+ /** paseo.json scripts as the daemon reports them (protocol WorkspaceScriptPayloadSchema). */
31
+ export interface ScriptInfo {
32
+ scriptName: string;
33
+ type?: "script" | "service";
34
+ port: number | null;
35
+ proxyUrl?: string | null;
36
+ localProxyUrl?: string | null;
37
+ lifecycle: "running" | "stopped";
38
+ health: "healthy" | "unhealthy" | null;
39
+ exitCode?: number | null;
40
+ terminalId?: string | null;
41
+ }
42
+
43
+ export interface WorkspaceScripts {
44
+ list(workspaceId: string): Promise<ScriptInfo[]>;
45
+ start(workspaceId: string, name: string): Promise<void>;
46
+ stop(workspaceId: string, name: string): Promise<void>;
47
+ /** paseo.json setup of a new worktree; null when the daemon does not report one. */
48
+ setupStatus(workspaceId: string): Promise<{ status: "running" | "completed" | "failed" | "blocked"; error: string | null } | null>;
49
+ /** Paseo schedules (daemon-client scheduleCreate / scheduleDelete). Returns the schedule ID. */
50
+ createSchedule(options: Record<string, unknown>): Promise<string>;
51
+ deleteSchedule(id: string): Promise<void>;
52
+ close(): Promise<void>;
53
+ }
54
+
55
+ export { samePath };
56
+
57
+ function agentConfig(profile: AgentProfile) {
58
+ if (!isResolved(profile)) {
59
+ throw new Error(`agent 配置没有 provider${profile.paseoProfile ? `(Paseo profile「${profile.paseoProfile}」没有解析)` : ""}`);
60
+ }
61
+ return {
62
+ provider: profile.provider,
63
+ ...(profile.modeId ? { modeId: profile.modeId } : {}),
64
+ ...(profile.thinkingOptionId ? { thinkingOptionId: profile.thinkingOptionId } : {}),
65
+ ...(profile.featureValues ? { featureValues: profile.featureValues } : {}),
66
+ ...(profile.options ? { options: profile.options as Record<string, never> } : {}),
67
+ };
68
+ }
69
+
70
+ function wrap(handle: PaseoAgentHandle): StepAgent {
71
+ return {
72
+ id: handle.id,
73
+ async wait(timeoutMs: number): Promise<WaitOutcome> {
74
+ const result = await handle.waitForFinish(timeoutMs);
75
+ const pending = result.final?.pendingPermissions ?? [];
76
+ return {
77
+ status: result.status,
78
+ error: result.error,
79
+ lastMessage: result.lastMessage,
80
+ pendingPermissions: pending.map((p) => ({
81
+ id: p.id,
82
+ title: p.title ?? p.description ?? p.name,
83
+ })),
84
+ usage: result.final?.lastUsage ?? null,
85
+ };
86
+ },
87
+ async followUp(text: string): Promise<void> {
88
+ await handle.send(text);
89
+ },
90
+ async archive(): Promise<void> {
91
+ await handle.archive();
92
+ },
93
+ };
94
+ }
95
+
96
+ async function agentCwd(handle: PaseoAgentHandle): Promise<string | null> {
97
+ const fetched = await handle.refresh();
98
+ return fetched?.agent.cwd ?? null;
99
+ }
100
+
101
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
102
+
103
+ export function createRuntimeWithClient(
104
+ client: SdkClient,
105
+ config: PipelineConfig,
106
+ log: (message: string) => void,
107
+ scriptsFactory: (() => Promise<WorkspaceScripts | null>) | null = null,
108
+ ): AgentRuntime {
109
+ let scripts: Promise<WorkspaceScripts | null> | null = null;
110
+ const getScripts = () => {
111
+ scripts ??= (scriptsFactory ? scriptsFactory() : Promise.resolve(null)).catch((error: Error) => {
112
+ log(`无法使用 Paseo 的工作区脚本:${error.message}`);
113
+ return null;
114
+ });
115
+ return scripts;
116
+ };
117
+ const workspaceOf = async (target: ServiceTarget): Promise<string | null> => {
118
+ if ("workspaceId" in target) return target.workspaceId;
119
+ const fetched = await client.agents.ref(target.agentId).refresh().catch(() => null);
120
+ return fetched?.agent.workspaceId ?? null;
121
+ };
122
+ const tail = async (terminalId: string | null | undefined): Promise<string | null> => {
123
+ if (!terminalId || !client.terminals) return null;
124
+ try {
125
+ const captured = await client.terminals.ref(terminalId).capture({ stripAnsi: true });
126
+ return captured.lines.slice(-60).join("\n").trim() || null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ };
131
+ const toService = async (info: ScriptInfo): Promise<ManagedService> => ({
132
+ name: info.scriptName,
133
+ url: info.proxyUrl ?? info.localProxyUrl ?? (info.port ? `http://127.0.0.1:${info.port}` : null),
134
+ port: info.port,
135
+ running: info.lifecycle === "running",
136
+ health: info.health ?? null,
137
+ exitCode: info.exitCode ?? null,
138
+ output: await tail(info.terminalId),
139
+ });
140
+
141
+ return {
142
+ async verifyOrigin(agentId, cwd) {
143
+ let originCwd: string | null;
144
+ try {
145
+ originCwd = await agentCwd(client.agents.ref(agentId));
146
+ } catch (error) {
147
+ throw new Error(`无法读取发起对话的 agent ${agentId}:${(error as Error).message}`);
148
+ }
149
+ if (originCwd === null) {
150
+ throw new Error(`找不到发起对话的 agent ${agentId}(可能已被归档)。请在需求所在项目的对话中重新启动。`);
151
+ }
152
+ if (!samePath(originCwd, cwd)) {
153
+ throw new Error(
154
+ `发起对话的工作目录是 ${originCwd},与仓库目录 ${cwd} 不一致。` +
155
+ "步骤 agent 会挂在发起的对话下、使用对话的工作目录,所以流水线必须在该仓库对应的项目对话中启动。",
156
+ );
157
+ }
158
+ },
159
+ async startAgent(input) {
160
+ const base = {
161
+ config: agentConfig(input.profile ?? config.agents[input.role]),
162
+ cwd: input.cwd,
163
+ title: input.title,
164
+ labels: input.labels,
165
+ prompt: input.prompt,
166
+ };
167
+ let handle: PaseoAgentHandle;
168
+ if (input.workspaceId) {
169
+ // The requirement's own worktree: placed there, still a subagent of the chat.
170
+ if (!client.workspaces) throw new Error("这个 Paseo 版本不支持把 agent 放进指定的工作区");
171
+ const { cwd: _cwd, ...placed } = base;
172
+ handle = await client.workspaces.ref(input.workspaceId).agents.create({
173
+ ...placed,
174
+ ...(input.parentAgentId ? { parent: input.parentAgentId } : {}),
175
+ });
176
+ } else {
177
+ handle = input.parentAgentId
178
+ ? await client.agents.create({ ...base, parent: input.parentAgentId })
179
+ : await client.agents.create(base);
180
+ }
181
+
182
+ // Guard against the daemon placing the agent anywhere but the repository.
183
+ const actual = await agentCwd(handle).catch(() => null);
184
+ if (actual !== null && !samePath(actual, input.cwd)) {
185
+ await handle.archive().catch(() => undefined);
186
+ throw new Error(`新建的 agent 工作目录是 ${actual},不是仓库目录 ${input.cwd},已停止该 agent`);
187
+ }
188
+ return wrap(handle);
189
+ },
190
+ async notifyAgent(agentId, text) {
191
+ await client.agents.ref(agentId).send(text, { activeTurnBehavior: "steer" });
192
+ },
193
+ async interruptAgent(agentId, text) {
194
+ await client.agents.ref(agentId).send(text, { activeTurnBehavior: "interrupt" });
195
+ },
196
+ async listPaseoProfiles() {
197
+ if (!client.config) return null;
198
+ try {
199
+ return profilesFromDaemonConfig((await client.config.get()).config);
200
+ } catch (error) {
201
+ log(`无法读取 Paseo 的 agent profiles:${(error as Error).message}`);
202
+ return null;
203
+ }
204
+ },
205
+ async listServices(target) {
206
+ const api = await getScripts();
207
+ const workspaceId = api ? await workspaceOf(target) : null;
208
+ if (!api || !workspaceId) return null;
209
+ try {
210
+ const services = (await api.list(workspaceId)).filter((s) => (s.type ?? "service") === "service").map((s) => s.scriptName);
211
+ return services.length ? services : null;
212
+ } catch (error) {
213
+ log(`无法列出工作区脚本:${(error as Error).message}`);
214
+ return null;
215
+ }
216
+ },
217
+ async startServices(target, names, readyTimeoutMs) {
218
+ const api = await getScripts();
219
+ const workspaceId = await workspaceOf(target);
220
+ if (!api || !workspaceId) throw new Error("无法使用 Paseo 的工作区脚本");
221
+ const current = await api.list(workspaceId);
222
+ for (const name of names) {
223
+ // Stop first so the service runs the code as it is now.
224
+ if (current.find((s) => s.scriptName === name)?.lifecycle === "running") await api.stop(workspaceId, name);
225
+ await api.start(workspaceId, name);
226
+ }
227
+ const deadline = Date.now() + readyTimeoutMs;
228
+ let latest = await api.list(workspaceId);
229
+ for (;;) {
230
+ const wanted = latest.filter((s) => names.includes(s.scriptName));
231
+ const settled = wanted.every((s) => s.lifecycle === "stopped" || s.health === "healthy");
232
+ if (settled || Date.now() >= deadline) break;
233
+ await sleep(2000);
234
+ latest = await api.list(workspaceId);
235
+ }
236
+ return Promise.all(latest.filter((s) => names.includes(s.scriptName)).map(toService));
237
+ },
238
+ async stopServices(target, names) {
239
+ const api = await getScripts();
240
+ const workspaceId = await workspaceOf(target);
241
+ if (!api || !workspaceId) return;
242
+ const current = await api.list(workspaceId).catch(() => []);
243
+ for (const name of names) {
244
+ if (current.find((s) => s.scriptName === name)?.lifecycle !== "running") continue;
245
+ await api.stop(workspaceId, name).catch((error: Error) => log(`停止服务 ${name} 失败:${error.message}`));
246
+ }
247
+ },
248
+ async providerQuota(providers) {
249
+ if (!client.providers) return null;
250
+ try {
251
+ const usage = await client.providers.listUsage();
252
+ const quotas: ProviderQuota[] = [];
253
+ for (const p of usage.providers) {
254
+ if (!providers.includes(p.providerId) || p.status !== "available") continue;
255
+ for (const w of p.windows) {
256
+ quotas.push({
257
+ provider: p.providerId,
258
+ displayName: p.displayName,
259
+ window: w.label,
260
+ usedPct: w.usedPct ?? (typeof w.remainingPct === "number" ? 100 - w.remainingPct : null),
261
+ resetsAt: w.resetsAt ?? null,
262
+ tone: w.tone ?? null,
263
+ });
264
+ }
265
+ }
266
+ return quotas.length ? quotas : null;
267
+ } catch {
268
+ return null;
269
+ }
270
+ },
271
+ async recentActivity(agentId, limit) {
272
+ try {
273
+ const page = await client.agents.ref(agentId).timeline.refetch({ direction: "tail", limit: Math.max(limit * 4, 20) });
274
+ return summarizeActivity(page.entries, limit);
275
+ } catch {
276
+ return null;
277
+ }
278
+ },
279
+ async createWorktree(request) {
280
+ if (!client.workspaces) throw new Error("这个 Paseo 版本不支持新建工作区");
281
+ const workspace = await client.workspaces.create({
282
+ source: { kind: "worktree", cwd: request.sourceRepo, action: "branch-off", refName: request.baseRef, branchName: request.branchName },
283
+ title: request.title,
284
+ ...(request.callerAgentId ? { callerAgentId: request.callerAgentId } : {}),
285
+ });
286
+ const directory = workspace.directory ?? (await workspace.refresh())?.workspaceDirectory ?? null;
287
+ if (!directory) throw new Error("Paseo 没有返回新工作区的目录");
288
+ return { workspaceId: workspace.id, directory };
289
+ },
290
+ async waitForWorkspaceSetup(workspaceId, timeoutMs) {
291
+ const api = await getScripts();
292
+ if (!api) return { status: "none", error: null };
293
+ const deadline = Date.now() + timeoutMs;
294
+ for (;;) {
295
+ const snapshot = await api.setupStatus(workspaceId).catch(() => null);
296
+ if (!snapshot) return { status: "none", error: null };
297
+ if (snapshot.status === "completed") return { status: "completed", error: null };
298
+ if (snapshot.status === "failed" || snapshot.status === "blocked") return { status: "failed", error: snapshot.error ?? snapshot.status };
299
+ if (Date.now() >= deadline) return { status: "timeout", error: null };
300
+ await sleep(3000);
301
+ }
302
+ },
303
+ async scheduleMessage(input) {
304
+ const api = await getScripts();
305
+ if (!api) return null;
306
+ const oneShot = input.at !== undefined;
307
+ return api.createSchedule({
308
+ name: input.name,
309
+ prompt: input.prompt,
310
+ cadence: { type: "cron", expression: oneShot ? cronAt(input.at as Date) : (input.cron as string), timezone: localTimeZone() },
311
+ target: { type: "agent", agentId: input.agentId },
312
+ ...(oneShot ? { maxRuns: 1, expiresAt: new Date((input.at as Date).getTime() + 6 * 3600_000).toISOString() } : {}),
313
+ ...(input.expiresAt ? { expiresAt: input.expiresAt.toISOString() } : {}),
314
+ });
315
+ },
316
+ async scheduleNewAgent(input) {
317
+ const api = await getScripts();
318
+ if (!api) return null;
319
+ const settings = agentConfig(input.profile);
320
+ const slash = settings.provider.indexOf("/");
321
+ return api.createSchedule({
322
+ name: input.name,
323
+ prompt: input.prompt,
324
+ cadence: { type: "cron", expression: input.cron, timezone: localTimeZone() },
325
+ target: {
326
+ type: "new-agent",
327
+ config: {
328
+ provider: settings.provider.slice(0, slash),
329
+ model: settings.provider.slice(slash + 1),
330
+ cwd: input.cwd,
331
+ ...(settings.modeId ? { modeId: settings.modeId } : {}),
332
+ ...(settings.thinkingOptionId ? { thinkingOptionId: settings.thinkingOptionId } : {}),
333
+ archiveOnFinish: true,
334
+ title: input.name,
335
+ },
336
+ },
337
+ });
338
+ },
339
+ async cancelSchedule(id) {
340
+ const api = await getScripts();
341
+ if (api) await api.deleteSchedule(id);
342
+ },
343
+ async capabilities() {
344
+ if (!client.config) return null;
345
+ try {
346
+ const daemonConfig = (await client.config.get()).config as { mcp?: { injectIntoAgents?: boolean }; browserTools?: { enabled?: boolean } };
347
+ const paseoTools = daemonConfig.mcp?.injectIntoAgents === true;
348
+ return { paseoTools, browser: paseoTools && daemonConfig.browserTools?.enabled === true };
349
+ } catch {
350
+ return null;
351
+ }
352
+ },
353
+ async close() {
354
+ if (scripts) await (await scripts)?.close().catch(() => undefined);
355
+ await client.close();
356
+ },
357
+ };
358
+ }
359
+
360
+ export const createRuntime: RuntimeFactory = async ({ config, log }) => {
361
+ // Loaded lazily so the runtime logic can be tested without the SDK installed.
362
+ const { createPaseoClient } = await import("@getpaseo/client");
363
+ const password = process.env[config.daemonPasswordEnv];
364
+ const client = createPaseoClient({
365
+ url: config.daemonUrl,
366
+ ...(password ? { password } : {}),
367
+ reconnect: { enabled: true },
368
+ });
369
+ await client.connect();
370
+ return createRuntimeWithClient(client, config, log, () => connectScripts(config));
371
+ };
372
+
373
+ /** Workspace scripts through the internal DaemonClient that @getpaseo/client 0.11 exports. */
374
+ async function connectScripts(config: PipelineConfig): Promise<WorkspaceScripts | null> {
375
+ const internal = (await import("@getpaseo/client/internal/daemon-client").catch(() => null)) as
376
+ | { DaemonClient?: new (options: Record<string, unknown>) => InternalDaemonClient }
377
+ | null;
378
+ if (!internal?.DaemonClient) return null;
379
+ const password = process.env[config.daemonPasswordEnv];
380
+ const daemon = new internal.DaemonClient({
381
+ url: config.daemonUrl,
382
+ clientId: `req-kit-${process.pid}-${Date.now().toString(36)}`,
383
+ clientType: "cli",
384
+ ...(password ? { password } : {}),
385
+ reconnect: { enabled: true },
386
+ });
387
+ if (typeof daemon.listWorkspaceScripts !== "function") return null;
388
+ await daemon.connect();
389
+ const check = (payload: { error: string | null }) => {
390
+ if (payload.error) throw new Error(payload.error);
391
+ };
392
+ return {
393
+ async list(workspaceId) {
394
+ const payload = await daemon.listWorkspaceScripts(workspaceId);
395
+ check(payload);
396
+ return (payload.scripts ?? []) as ScriptInfo[];
397
+ },
398
+ async start(workspaceId, name) {
399
+ check(await daemon.startWorkspaceScriptWithStatus(workspaceId, name));
400
+ },
401
+ async stop(workspaceId, name) {
402
+ check(await daemon.stopWorkspaceScript(workspaceId, name));
403
+ },
404
+ async createSchedule(options) {
405
+ if (typeof daemon.scheduleCreate !== "function") throw new Error("这个 Paseo 版本不支持定时任务");
406
+ const payload = await daemon.scheduleCreate(options);
407
+ if (payload.error || !payload.schedule) throw new Error(payload.error ?? "Paseo 没有返回定时任务");
408
+ return payload.schedule.id;
409
+ },
410
+ async deleteSchedule(id) {
411
+ if (typeof daemon.scheduleDelete !== "function") return;
412
+ const payload = await daemon.scheduleDelete({ id });
413
+ if (payload.error && !/not found/i.test(payload.error)) throw new Error(payload.error);
414
+ },
415
+ async setupStatus(workspaceId) {
416
+ if (typeof daemon.fetchWorkspaceSetupStatus !== "function") return null;
417
+ const payload = await daemon.fetchWorkspaceSetupStatus(workspaceId);
418
+ return payload.snapshot ? { status: payload.snapshot.status, error: payload.snapshot.error } : null;
419
+ },
420
+ async close() {
421
+ await daemon.close();
422
+ },
423
+ };
424
+ }
425
+
426
+ interface InternalDaemonClient {
427
+ connect(): Promise<void>;
428
+ close(): Promise<void>;
429
+ listWorkspaceScripts(workspaceId: string): Promise<{ scripts?: unknown[]; error: string | null }>;
430
+ startWorkspaceScriptWithStatus(workspaceId: string, name: string): Promise<{ error: string | null }>;
431
+ stopWorkspaceScript(workspaceId: string, name: string): Promise<{ error: string | null }>;
432
+ scheduleCreate?(options: Record<string, unknown>): Promise<{ schedule: { id: string } | null; error: string | null }>;
433
+ scheduleDelete?(options: { id: string }): Promise<{ error: string | null }>;
434
+ fetchWorkspaceSetupStatus?(
435
+ workspaceId: string,
436
+ ): Promise<{ snapshot: { status: "running" | "completed" | "failed" | "blocked"; error: string | null } | null }>;
437
+ }
@@ -0,0 +1,10 @@
1
+ import path from "node:path";
2
+
3
+ /** Compares directories the way the host filesystem does (case-insensitive on Windows and macOS). */
4
+ export function samePath(a: string, b: string, platform: NodeJS.Platform = process.platform): boolean {
5
+ const norm = (p: string) => {
6
+ const resolved = (platform === "win32" ? path.win32 : path.posix).resolve(p).replace(/[\\/]+$/, "");
7
+ return platform === "win32" || platform === "darwin" ? resolved.toLowerCase() : resolved;
8
+ };
9
+ return norm(a) === norm(b);
10
+ }