paseo-req-kit 0.0.0-stage → 1.8.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 (144) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/LICENSE +21 -0
  3. package/README.md +146 -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 +179 -0
  8. package/bin/start-pipeline +21 -0
  9. package/bin/start-pipeline.mjs +22 -0
  10. package/config.example.json +132 -0
  11. package/contracts/README.md +18 -0
  12. package/contracts/feature-files.md +117 -0
  13. package/contracts/notifier-event.md +72 -0
  14. package/contracts/result-file.md +21 -0
  15. package/contracts/spec.md +36 -0
  16. package/contracts/state.md +49 -0
  17. package/contracts/verify.md +41 -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/0023-types-gate-slices.md +54 -0
  41. package/docs/adr/0024-confirm-early-review-axes.md +27 -0
  42. package/docs/adr/0025-joint-requirements.md +27 -0
  43. package/docs/adr/README.md +29 -0
  44. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +96 -0
  45. package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +348 -0
  46. package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +85 -0
  47. 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 +356 -0
  48. package/engine/prompts/00-shared-context.md +104 -0
  49. package/engine/prompts/02-spec-review.md +46 -0
  50. package/engine/prompts/03-design.md +73 -0
  51. package/engine/prompts/03a-design-proposal.md +30 -0
  52. package/engine/prompts/04-design-review.md +73 -0
  53. package/engine/prompts/04b-design-revise.md +41 -0
  54. package/engine/prompts/05-implement.md +72 -0
  55. package/engine/prompts/06-code-review.md +70 -0
  56. package/engine/prompts/07-review-fix.md +41 -0
  57. package/engine/prompts/07b-handoff.md +45 -0
  58. package/engine/prompts/08-service-start.md +59 -0
  59. package/engine/prompts/09-verify.md +90 -0
  60. package/engine/prompts/10-defect-fix.md +41 -0
  61. package/engine/prompts/10b-reverify.md +43 -0
  62. package/engine/prompts/11-report.md +112 -0
  63. package/engine/prompts/browser-verify.md +13 -0
  64. package/engine/prompts/incremental.md +22 -0
  65. package/engine/prompts/joint.md +55 -0
  66. package/engine/prompts/panel-member.md +38 -0
  67. package/engine/prompts/paseo-services.md +9 -0
  68. package/engine/prompts/related-repos.md +7 -0
  69. package/engine/prompts/review-axis-spec.md +10 -0
  70. package/engine/prompts/review-axis-standards.md +11 -0
  71. package/engine/prompts/types/assessment.md +41 -0
  72. package/engine/prompts/types/defect.md +55 -0
  73. package/engine/prompts/types/refactor.md +39 -0
  74. package/engine/prompts/verify-deployed.md +14 -0
  75. package/engine/src/activity.ts +87 -0
  76. package/engine/src/cli-args.ts +217 -0
  77. package/engine/src/config.ts +619 -0
  78. package/engine/src/configure.ts +237 -0
  79. package/engine/src/control.ts +246 -0
  80. package/engine/src/daemon-check.ts +104 -0
  81. package/engine/src/daemon-version.ts +66 -0
  82. package/engine/src/digest.ts +142 -0
  83. package/engine/src/doctor.ts +462 -0
  84. package/engine/src/eval.ts +324 -0
  85. package/engine/src/features.ts +270 -0
  86. package/engine/src/gate.ts +157 -0
  87. package/engine/src/git.ts +30 -0
  88. package/engine/src/joint.ts +173 -0
  89. package/engine/src/launch.ts +405 -0
  90. package/engine/src/lessons.ts +72 -0
  91. package/engine/src/locations.ts +55 -0
  92. package/engine/src/main.ts +346 -0
  93. package/engine/src/messages.ts +394 -0
  94. package/engine/src/notifiers.ts +76 -0
  95. package/engine/src/notify-test.ts +101 -0
  96. package/engine/src/paseo-json.ts +73 -0
  97. package/engine/src/paseo-profiles.ts +133 -0
  98. package/engine/src/paseo-runtime.ts +437 -0
  99. package/engine/src/paths.ts +10 -0
  100. package/engine/src/pipeline.ts +1840 -0
  101. package/engine/src/playbooks.ts +46 -0
  102. package/engine/src/preflight.ts +337 -0
  103. package/engine/src/prompts.ts +72 -0
  104. package/engine/src/queue.ts +61 -0
  105. package/engine/src/runtime-loader.ts +14 -0
  106. package/engine/src/runtime.ts +155 -0
  107. package/engine/src/schedule-time.ts +51 -0
  108. package/engine/src/smoke-test.ts +202 -0
  109. package/engine/src/spec-review.ts +100 -0
  110. package/engine/src/state.ts +333 -0
  111. package/engine/src/steps.ts +266 -0
  112. package/engine/src/usage.ts +67 -0
  113. package/engine/src/worktree.ts +72 -0
  114. package/install.mjs +928 -0
  115. package/notifiers/im-template.mjs +60 -0
  116. package/package.json +67 -4
  117. package/principles/INDEX.md +11 -0
  118. package/skills/acceptance-criteria/SKILL.md +61 -0
  119. package/skills/principle-delete-before-add/SKILL.md +33 -0
  120. package/skills/principle-dont-block-on-humans/SKILL.md +35 -0
  121. package/skills/principle-fix-root-cause/SKILL.md +33 -0
  122. package/skills/principle-prove-it-works/SKILL.md +33 -0
  123. package/skills/principle-report-honestly/SKILL.md +33 -0
  124. package/skills/principle-speak-to-reader/SKILL.md +33 -0
  125. package/skills/principle-this-requirement-only/SKILL.md +33 -0
  126. package/skills/req/SKILL.md +101 -0
  127. package/skills/req/playbooks/accept.md +65 -0
  128. package/skills/req/playbooks/change.md +51 -0
  129. package/skills/req/playbooks/incident.md +55 -0
  130. package/skills/req/playbooks/new.md +94 -0
  131. package/skills/req/playbooks/small.md +48 -0
  132. package/skills/req/playbooks/status.md +41 -0
  133. package/skills/req/playbooks/takeover.md +51 -0
  134. package/skills/req/playbooks/verify.md +57 -0
  135. package/skills/req-doctor/SKILL.md +36 -0
  136. package/skills/req-retro/SKILL.md +81 -0
  137. package/skills/req-setup/SKILL.md +118 -0
  138. package/skills/req-setup/templates/paseo/copy-local-files.mjs +32 -0
  139. package/skills/req-setup/templates/paseo/with-port.mjs +36 -0
  140. package/skills/requirement-interview/SKILL.md +97 -0
  141. package/skills/requirement-kickoff/SKILL.md +17 -0
  142. package/skills/result-report/SKILL.md +34 -0
  143. package/skills/spec-writing/SKILL.md +55 -0
  144. package/skills/spec-writing/spec-template.md +109 -0
@@ -0,0 +1,133 @@
1
+ // Paseo agent profiles: named launch bundles (provider, model, mode, thinking level,
2
+ // features) that people set up once in Paseo's settings. req-kit roles and review
3
+ // panel members can name one instead of spelling the settings out:
4
+ //
5
+ // "builder": { "paseoProfile": "实现" }
6
+ // "reviewer": { "paseoProfile": "评审", "thinkingOptionId": "high" } // own fields win
7
+ //
8
+ // The names are resolved against the daemon when a run starts, so changing the
9
+ // profile in Paseo changes the next run without touching req-kit's config.
10
+
11
+ import type { AgentProfile, PipelineConfig } from "./config.js";
12
+
13
+ /** The fields of Paseo's AgentProfile that req-kit uses (protocol/src/agent-profile.ts). */
14
+ export interface PaseoProfile {
15
+ id: string;
16
+ name: string;
17
+ provider: string;
18
+ model?: string;
19
+ modeId?: string;
20
+ thinkingOptionId?: string;
21
+ featureValues?: Record<string, unknown>;
22
+ notes?: string;
23
+ }
24
+
25
+ export class ProfileResolutionError extends Error {
26
+ constructor(message: string) {
27
+ super(message);
28
+ this.name = "ProfileResolutionError";
29
+ }
30
+ }
31
+
32
+ export function findPaseoProfile(name: string, available: PaseoProfile[]): PaseoProfile | null {
33
+ const wanted = name.trim();
34
+ return (
35
+ available.find((p) => p.name === wanted) ??
36
+ available.find((p) => p.name.toLowerCase() === wanted.toLowerCase()) ??
37
+ available.find((p) => p.id === wanted) ??
38
+ null
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Turns a role that names a Paseo profile into concrete agent settings. Settings
44
+ * written next to `paseoProfile` override the profile's; feature values are merged.
45
+ * Roles without `paseoProfile` are returned unchanged.
46
+ */
47
+ export function resolveAgentProfile(label: string, profile: AgentProfile, available: PaseoProfile[] | null): AgentProfile {
48
+ if (!profile.paseoProfile) return profile;
49
+ if (available === null) {
50
+ throw new ProfileResolutionError(`${label} 使用 Paseo profile「${profile.paseoProfile}」,但无法从 daemon 读取 profile 列表`);
51
+ }
52
+ const found = findPaseoProfile(profile.paseoProfile, available);
53
+ if (!found) {
54
+ const names = available.map((p) => `「${p.name}」`).join("、") || "(还没有任何 profile)";
55
+ throw new ProfileResolutionError(
56
+ `${label} 使用的 Paseo profile「${profile.paseoProfile}」不存在。现有的 profile:${names}。在 Paseo 的 设置 → Agents → Agent profiles 中查看或新建`,
57
+ );
58
+ }
59
+ if (!found.model) {
60
+ throw new ProfileResolutionError(`Paseo profile「${found.name}」没有选择模型,req-kit 需要明确的模型。请在 Paseo 设置中为它选择模型`);
61
+ }
62
+ const resolved: AgentProfile = { ...profile, provider: profile.provider ?? `${found.provider}/${found.model}` };
63
+ const modeId = profile.modeId ?? found.modeId;
64
+ const thinking = profile.thinkingOptionId ?? found.thinkingOptionId;
65
+ const features = found.featureValues || profile.featureValues ? { ...found.featureValues, ...profile.featureValues } : undefined;
66
+ if (modeId) resolved.modeId = modeId;
67
+ if (thinking) resolved.thinkingOptionId = thinking;
68
+ if (features) resolved.featureValues = features;
69
+ return resolved;
70
+ }
71
+
72
+ /** "Paseo profile「实现」→ claude/opus(模式 bypassPermissions)" */
73
+ export function describeAgentProfile(profile: AgentProfile): string {
74
+ if (!profile.provider) return `Paseo profile「${profile.paseoProfile}」(运行时从 Paseo 读取设置)`;
75
+ const settings = `${profile.provider}${profile.modeId ? `(模式 ${profile.modeId})` : ""}`;
76
+ return profile.paseoProfile ? `Paseo profile「${profile.paseoProfile}」→ ${settings}` : settings;
77
+ }
78
+
79
+ /** Reads the daemon's profiles from its config. Null when the daemon does not say. */
80
+ export function profilesFromDaemonConfig(config: unknown): PaseoProfile[] | null {
81
+ if (typeof config !== "object" || config === null) return null;
82
+ const list = (config as { agentProfiles?: unknown }).agentProfiles;
83
+ if (list === undefined) return [];
84
+ if (!Array.isArray(list)) return null;
85
+ return list.filter(
86
+ (p): p is PaseoProfile =>
87
+ typeof p === "object" && p !== null && typeof (p as PaseoProfile).name === "string" && typeof (p as PaseoProfile).provider === "string",
88
+ );
89
+ }
90
+
91
+ /** Whether any role or panel member names a Paseo profile. */
92
+ export function usesPaseoProfiles(config: PipelineConfig): boolean {
93
+ return [
94
+ config.agents.builder,
95
+ config.agents.reviewer,
96
+ ...config.reviewPanel.members,
97
+ ...config.designProposals.members,
98
+ ...Object.values(config.phases.verify.agents),
99
+ ].some((p) =>
100
+ Boolean(p.paseoProfile),
101
+ );
102
+ }
103
+
104
+ /** The config with every Paseo profile name replaced by its settings. Throws ProfileResolutionError. */
105
+ export function resolveConfigProfiles(config: PipelineConfig, available: PaseoProfile[] | null): PipelineConfig {
106
+ return {
107
+ ...config,
108
+ agents: {
109
+ builder: resolveAgentProfile("agents.builder", config.agents.builder, available),
110
+ reviewer: resolveAgentProfile("agents.reviewer", config.agents.reviewer, available),
111
+ },
112
+ reviewPanel: {
113
+ ...config.reviewPanel,
114
+ members: config.reviewPanel.members.map((m, i) => ({ ...m, ...resolveAgentProfile(`reviewPanel.members[${i}]`, m, available) })),
115
+ },
116
+ designProposals: {
117
+ ...config.designProposals,
118
+ members: config.designProposals.members.map((m, i) => ({
119
+ ...m,
120
+ ...resolveAgentProfile(`designProposals.members[${i}]`, m, available),
121
+ })),
122
+ },
123
+ phases: {
124
+ ...config.phases,
125
+ verify: {
126
+ ...config.phases.verify,
127
+ agents: Object.fromEntries(
128
+ Object.entries(config.phases.verify.agents).map(([role, p]) => [role, resolveAgentProfile(`phases.verify.agents.${role}`, p, available)]),
129
+ ),
130
+ },
131
+ },
132
+ };
133
+ }
@@ -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
+ }