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,76 @@
1
+ // External notifiers: local modules the engine calls at key events, in addition to
2
+ // the message it sends into the originating chat. This is the slot for an internal
3
+ // IM (or any other channel); see notifiers/im-template.mjs and contracts/notifier-event.md.
4
+ //
5
+ // A notifier never affects the pipeline: errors and timeouts are logged and ignored.
6
+
7
+ import { pathToFileURL } from "node:url";
8
+
9
+ export const NOTIFIER_EVENT_TYPES = ["milestone", "blocked", "permission", "failed", "completed", "digest", "phase_done"] as const;
10
+ export type NotifierEventType = (typeof NOTIFIER_EVENT_TYPES)[number];
11
+
12
+ export interface NotifierConfig {
13
+ /** Absolute path of the module (resolved when the config is loaded). */
14
+ module: string;
15
+ /** Events to send; all of them when omitted. */
16
+ events?: NotifierEventType[];
17
+ /** Passed to the module as its second argument, e.g. a webhook address or a group ID. */
18
+ options?: Record<string, unknown>;
19
+ }
20
+
21
+ export interface NotifierEvent {
22
+ /** Format version of this object. */
23
+ schema: 1;
24
+ type: NotifierEventType;
25
+ at: string;
26
+ kitVersion: string;
27
+ feature: { dir: string; name: string; title: string };
28
+ repo: string;
29
+ branch: string | null;
30
+ profile: string;
31
+ /** Chat agent the run reports to, if any. */
32
+ originAgentId: string | null;
33
+ /** One or two plain sentences, ready to post as is. */
34
+ text: string;
35
+ /** Type-specific fields; see contracts/notifier-event.md. */
36
+ details: Record<string, unknown>;
37
+ }
38
+
39
+ export type NotifierFunction = (event: NotifierEvent, options: Record<string, unknown>) => unknown;
40
+
41
+ const TIMEOUT_MS = 15_000;
42
+
43
+ async function loadNotifier(file: string): Promise<NotifierFunction> {
44
+ const module = (await import(pathToFileURL(file).href)) as { default?: unknown; notify?: unknown };
45
+ const fn = module.default ?? module.notify;
46
+ if (typeof fn !== "function") throw new Error(`${file} 没有导出 notify 函数(export default 或 export function notify)`);
47
+ return fn as NotifierFunction;
48
+ }
49
+
50
+ /** Calls every configured notifier that wants this event. Never throws. */
51
+ export async function dispatchNotifiers(
52
+ notifiers: NotifierConfig[],
53
+ event: NotifierEvent,
54
+ log: (message: string) => void,
55
+ ): Promise<void> {
56
+ const wanted = notifiers.filter((n) => !n.events || n.events.includes(event.type));
57
+ await Promise.all(
58
+ wanted.map(async (n) => {
59
+ let timer: NodeJS.Timeout | undefined;
60
+ try {
61
+ const fn = await loadNotifier(n.module);
62
+ await Promise.race([
63
+ Promise.resolve(fn(event, n.options ?? {})),
64
+ new Promise((_, reject) => {
65
+ timer = setTimeout(() => reject(new Error(`超过 ${TIMEOUT_MS / 1000} 秒没有完成`)), TIMEOUT_MS);
66
+ }),
67
+ ]);
68
+ log(`外部通知已发送(${event.type}):${n.module}`);
69
+ } catch (error) {
70
+ log(`外部通知失败(${event.type},${n.module}):${(error as Error).message}。不影响流水线。`);
71
+ } finally {
72
+ if (timer) clearTimeout(timer);
73
+ }
74
+ }),
75
+ );
76
+ }
@@ -0,0 +1,97 @@
1
+ // Sends one test event to every configured notifier: `req notify-test [--type blocked] [--repo <dir>]`.
2
+ // Lets whoever implements the IM module check it without running a pipeline.
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ import { loadConfig } from "./config.js";
8
+ import { defaultConfigPath, kitVersion } from "./locations.js";
9
+ import { dispatchNotifiers, NOTIFIER_EVENT_TYPES, type NotifierEvent, type NotifierEventType } from "./notifiers.js";
10
+
11
+ /** A realistic event of the given type, with the details the pipeline would send. */
12
+ export function sampleEvent(type: NotifierEventType, repo: string): NotifierEvent {
13
+ const featureDir = "docs/features/20261012-sms-login";
14
+ const resume = `node "<req-kit>/bin/start-pipeline.mjs" --feature ${featureDir} --resume`;
15
+ const samples: Record<NotifierEventType, { text: string; details: Record<string, unknown> }> = {
16
+ milestone: { text: "技术方案已经确定(审核 1 轮),开始写代码。", details: { milestone: "design-settled" } },
17
+ blocked: {
18
+ text: "方案审核发现 1 个需要需求方决定的问题,流水线已暂停,请到发起的对话中回答。",
19
+ details: { questions: ["验证码有效期是 5 分钟还是 10 分钟?"], resumeCommand: resume },
20
+ },
21
+ permission: {
22
+ text: "「实现」步骤在等待权限批准,批准前会一直停在这里。",
23
+ details: { stepTitle: "实现", agentTitle: "实现 · 短信验证码登录", requests: ["运行 npm install"] },
24
+ },
25
+ failed: {
26
+ text: "开发流水线在「代码评审」步骤中断:agent 连续出错。",
27
+ details: { stepTitle: "代码评审", reason: "agent 连续出错", logPath: `${featureDir}/.pipeline/pipeline.log`, resumeCommand: resume },
28
+ },
29
+ digest: {
30
+ text: "【req-kit 需求总览】shop\n共 3 个需求:需要处理 1,待验收 1,开发中 1。",
31
+ details: { features: [{ name: "20261012-sms-login", title: "短信验证码登录", stage: "待验收" }] },
32
+ },
33
+ phase_done: {
34
+ text: `研发已完成,可以提测:提测说明在 ${featureDir}/handoff.md。`,
35
+ details: { handoffPath: `${featureDir}/handoff.md`, mode: "gated", verifyCommand: `${resume} --phase verify`, verifyAt: null, handbacks: 0 },
36
+ },
37
+ completed: {
38
+ text: "开发流水线已结束:已完成,可以验收。",
39
+ details: { outcome: "completed", summary: "6 条验收标准全部通过。", reportPath: `${featureDir}/report.md` },
40
+ },
41
+ };
42
+ return {
43
+ schema: 1,
44
+ type,
45
+ at: new Date().toISOString(),
46
+ kitVersion: kitVersion(),
47
+ feature: { dir: featureDir, name: "20261012-sms-login", title: "短信验证码登录(测试消息)" },
48
+ repo,
49
+ branch: "feature/20261012-sms-login",
50
+ profile: "standard",
51
+ originAgentId: null,
52
+ text: `【测试】${samples[type].text}`,
53
+ details: samples[type].details,
54
+ };
55
+ }
56
+
57
+ async function main(): Promise<number> {
58
+ const { values } = parseArgs({
59
+ args: process.argv.slice(2),
60
+ options: { type: { type: "string", default: "completed" }, repo: { type: "string" }, config: { type: "string" } },
61
+ });
62
+ const type = values.type as NotifierEventType;
63
+ if (!NOTIFIER_EVENT_TYPES.includes(type)) {
64
+ console.error(`--type 只能是 ${NOTIFIER_EVENT_TYPES.join("、")} 之一`);
65
+ return 2;
66
+ }
67
+ const repo = path.resolve(values.repo ?? process.cwd());
68
+ const configPath = values.config ? path.resolve(values.config) : defaultConfigPath();
69
+ const { config } = loadConfig(configPath, fs.existsSync(repo) ? repo : undefined);
70
+ if (config.notifiers.length === 0) {
71
+ console.log(`没有配置外部通知(${configPath} 中的 notifiers 为空)。写法见 notifiers/im-template.mjs 开头的说明。`);
72
+ return 1;
73
+ }
74
+ const wanted = config.notifiers.filter((n) => !n.events || n.events.includes(type));
75
+ if (wanted.length === 0) {
76
+ console.log(`配置的通知都没有订阅 ${type} 事件。可以用 --type 换一种,例如 --type ${config.notifiers[0].events?.[0] ?? "completed"}。`);
77
+ return 1;
78
+ }
79
+ let failed = false;
80
+ // Lets a module show what it would send (the template does) without a real IM.
81
+ process.env.REQ_KIT_NOTIFY_TEST = "1";
82
+ await dispatchNotifiers(config.notifiers, sampleEvent(type, repo), (message) => {
83
+ if (message.startsWith("外部通知失败")) failed = true;
84
+ console.log(message);
85
+ });
86
+ return failed ? 1 : 0;
87
+ }
88
+
89
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
90
+ main().then(
91
+ (code) => process.exit(code),
92
+ (error: Error) => {
93
+ console.error(error.message);
94
+ process.exit(1);
95
+ },
96
+ );
97
+ }
@@ -0,0 +1,73 @@
1
+ // paseo.json checks shared by the doctor.
2
+ //
3
+ // Paseo 0.11 runs project commands with a different shell on each system:
4
+ // worktree.setup: bash (macOS, Linux) / powershell -ExecutionPolicy Bypass (Windows)
5
+ // scripts: the terminal's shell, $SHELL or /bin/sh (macOS, Linux) / cmd.exe (Windows)
6
+ // paseo.json is committed and shared, so commands should work in all of them: plain program
7
+ // invocations, with environment variables read by the program (see /req-setup's helpers).
8
+
9
+ export type PaseoJson = { worktree?: { setup?: unknown }; scripts?: Record<string, { type?: string; command?: string; port?: number }> };
10
+
11
+ export type ShellStyle = "posix" | "powershell" | "cmd";
12
+
13
+ const STYLE_NAMES: Record<ShellStyle, string> = { posix: "bash/sh", powershell: "PowerShell", cmd: "cmd.exe" };
14
+
15
+ /** Shell-specific syntax found in a command (empty: works the same everywhere). */
16
+ export function shellStyles(command: string): ShellStyle[] {
17
+ const styles = new Set<ShellStyle>();
18
+ if (/\$env:[A-Za-z_]/i.test(command) || /\b(Test-Path|Copy-Item|Set-Location|Get-ChildItem|Remove-Item)\b/.test(command)) styles.add("powershell");
19
+ if (/%[A-Za-z_][A-Za-z0-9_]*%/.test(command)) styles.add("cmd");
20
+ if (/(^|[^:\w`])\$\{?(?!env:)[A-Za-z_]/i.test(command) || /\[\s+-[a-z]\s/.test(command) || /(^|\s)(cp|mv|rm|export)\s/.test(command) || /\|\||&&/.test(command)) {
21
+ // `&&` works in cmd and PowerShell 7 too, but not in Windows PowerShell 5.1, which Paseo uses for setup.
22
+ styles.add("posix");
23
+ }
24
+ return [...styles];
25
+ }
26
+
27
+ function setupCommands(raw: PaseoJson): string[] {
28
+ const setup = raw.worktree?.setup;
29
+ if (Array.isArray(setup)) return setup.map(String);
30
+ return typeof setup === "string" ? setup.split(/\r?\n/).filter((l) => l.trim()) : [];
31
+ }
32
+
33
+ export interface PaseoJsonIssue {
34
+ /** "fails": the command will not work on this machine; "portable": it only works on some systems. */
35
+ kind: "fails" | "portable";
36
+ text: string;
37
+ fix: string;
38
+ }
39
+
40
+ const FIX =
41
+ "改成各系统都一样的写法:准备命令用 node scripts/paseo/copy-local-files.mjs .env,服务用 node scripts/paseo/with-port.mjs npm run dev -- --port {PORT}(/req-setup 会写入这两个脚本)";
42
+
43
+ /** Problems of the project's paseo.json for the system Paseo runs on (process.platform by default). */
44
+ export function paseoJsonIssues(raw: PaseoJson, platform: NodeJS.Platform = process.platform): PaseoJsonIssue[] {
45
+ const windows = platform === "win32";
46
+ const setupShell: ShellStyle = windows ? "powershell" : "posix";
47
+ const scriptShell: ShellStyle = windows ? "cmd" : "posix";
48
+ const issues: PaseoJsonIssue[] = [];
49
+ const check = (where: string, command: string, shell: ShellStyle) => {
50
+ const styles = shellStyles(command);
51
+ if (styles.length === 0) return;
52
+ const short = command.length > 60 ? `${command.slice(0, 60)}…` : command;
53
+ const foreign = styles.filter((s) => s !== shell);
54
+ if (foreign.length > 0) {
55
+ issues.push({
56
+ kind: "fails",
57
+ text: `${where}「${short}」是 ${foreign.map((s) => STYLE_NAMES[s]).join("、")} 写法,这台机器上 Paseo 用 ${STYLE_NAMES[shell]} 执行,会失败`,
58
+ fix: FIX,
59
+ });
60
+ } else {
61
+ issues.push({
62
+ kind: "portable",
63
+ text: `${where}「${short}」用了 ${STYLE_NAMES[shell]} 的写法,在这台机器上可以用,换到其他系统(Windows 和 macOS/Linux 之间)会失败`,
64
+ fix: FIX,
65
+ });
66
+ }
67
+ };
68
+ for (const cmd of setupCommands(raw)) check("paseo.json 的 worktree.setup 中", cmd, setupShell);
69
+ for (const [name, s] of Object.entries(raw.scripts ?? {})) {
70
+ check(`paseo.json 的${s.type === "service" ? "服务" : "脚本"} ${name}`, String(s.command ?? ""), scriptShell);
71
+ }
72
+ return issues;
73
+ }
@@ -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
+ }