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,142 @@
1
+ // Requirement overview for a project, for people who are not in the chat:
2
+ // req digest [--repo <dir>] print it
3
+ // req digest --notify also send it to the configured notifiers (event "digest")
4
+ // req digest --schedule "0 9 * * 1" have Paseo run "req digest --notify" on a schedule (fresh agent)
5
+ // req digest --cancel <schedule ID> stop that schedule
6
+ // The schedule starts a short-lived agent (the builder, or digest.profile) only to run the command.
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { parseArgs } from "node:util";
11
+ import { loadConfig } from "./config.js";
12
+ import { type FeatureSummary, listFeatures } from "./features.js";
13
+ import { handledCategories, type RecurringLesson, recurringLessons } from "./lessons.js";
14
+ import { defaultConfigPath, KIT_ROOT, kitVersion } from "./locations.js";
15
+ import { dispatchNotifiers } from "./notifiers.js";
16
+ import { resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
17
+ import { loadRuntimeFactory } from "./runtime-loader.js";
18
+
19
+ const GROUPS: Array<{ title: string; match: (stage: string) => boolean }> = [
20
+ {
21
+ title: "需要处理",
22
+ match: (s) => s.startsWith("等待需求方决定") || s.startsWith("等待确认方案") || s.startsWith("已中断") || s.startsWith("验收未通过") || s.startsWith("研发完成,待测试验证"),
23
+ },
24
+ { title: "待验收", match: (s) => s.startsWith("待验收") || s.startsWith("开发结束") },
25
+ { title: "开发中", match: (s) => s === "开发中" },
26
+ { title: "对齐中 / 未开始", match: (s) => s === "对齐中" || s.startsWith("已确认") },
27
+ { title: "已验收", match: (s) => s === "已验收" },
28
+ ];
29
+
30
+ export function digestText(project: string, features: FeatureSummary[], now = new Date(), days = 7, lessons: RecurringLesson[] = []): string {
31
+ const since = now.getTime() - days * 24 * 3600_000;
32
+ const recent = features.filter((f) => new Date(f.updatedAt).getTime() >= since);
33
+ const lines = [`【req-kit 需求总览】${project}(${now.toLocaleDateString("zh-CN")})`];
34
+ if (features.length === 0) return `${lines[0]}\n还没有需求。`;
35
+ const counts = GROUPS.map((g) => [g.title, features.filter((f) => g.match(f.stage)).length] as const).filter(([, n]) => n > 0);
36
+ lines.push(`共 ${features.length} 个需求:${counts.map(([t, n]) => `${t} ${n}`).join(",")};最近 ${days} 天有变化的 ${recent.length} 个。`);
37
+ for (const group of GROUPS) {
38
+ const items = features.filter((f) => group.match(f.stage) && (group.title !== "已验收" || recent.includes(f)));
39
+ if (items.length === 0) continue;
40
+ lines.push("", `${group.title}${group.title === "已验收" ? `(最近 ${days} 天)` : ""}:`);
41
+ for (const f of items) {
42
+ lines.push(`- ${f.title}(${f.name}):${f.stage}${f.branch ? `,分支 ${f.branch}` : ""}${f.usage ? `;${f.usage}` : ""}`);
43
+ }
44
+ }
45
+ if (lessons.length > 0) {
46
+ lines.push("", "反复出现的问题(建议变成项目的规则,可以运行 /req-retro):");
47
+ for (const l of lessons.slice(0, 5)) {
48
+ lines.push(`- ${l.category}:${l.features.length} 个需求(${l.features.join("、")})${l.rules.length > 0 ? `;建议:${l.rules[0]}` : ""}`);
49
+ }
50
+ }
51
+ return lines.join("\n");
52
+ }
53
+
54
+ async function main(): Promise<number> {
55
+ const { values } = parseArgs({
56
+ args: process.argv.slice(2),
57
+ options: {
58
+ repo: { type: "string" },
59
+ config: { type: "string" },
60
+ notify: { type: "boolean", default: false },
61
+ schedule: { type: "string" },
62
+ cancel: { type: "string" },
63
+ days: { type: "string", default: "7" },
64
+ },
65
+ });
66
+ const repo = fs.realpathSync(path.resolve(values.repo ?? process.env.PASEO_AGENT_CWD ?? process.cwd()));
67
+ const configPath = values.config ? path.resolve(values.config) : defaultConfigPath();
68
+ let config = loadConfig(configPath, repo).config;
69
+
70
+ if (values.schedule || values.cancel) {
71
+ const runtime = await (await loadRuntimeFactory())({ config, log: () => undefined });
72
+ try {
73
+ if (values.cancel) {
74
+ await runtime.cancelSchedule(values.cancel);
75
+ console.log(`已取消定时任务 ${values.cancel}。`);
76
+ return 0;
77
+ }
78
+ if (usesPaseoProfiles(config)) config = resolveConfigProfiles(config, await runtime.listPaseoProfiles());
79
+ const command = `node "${path.join(KIT_ROOT, "bin", "req.mjs").split(path.sep).join("/")}" digest --notify --repo "${repo}"${values.config ? ` --config "${configPath}"` : ""}`;
80
+ const id = await runtime.scheduleNewAgent({
81
+ name: `req-kit 需求总览:${path.basename(repo)}`,
82
+ cron: values.schedule as string,
83
+ cwd: repo,
84
+ profile: config.agents.builder,
85
+ prompt: `运行下面的命令,然后直接结束。不要做其他任何事,不要修改文件:\n\n${command}`,
86
+ });
87
+ if (!id) {
88
+ console.error("这个 Paseo 版本不支持定时任务。");
89
+ return 1;
90
+ }
91
+ console.log(`已创建定时任务 ${id}:按"${values.schedule}"(本机时区)把需求总览发到配置的外部通知。取消:req digest --cancel ${id}`);
92
+ return 0;
93
+ } finally {
94
+ await runtime.close().catch(() => undefined);
95
+ }
96
+ }
97
+
98
+ const features = listFeatures(repo);
99
+ const lessons = recurringLessons(features, handledCategories(repo));
100
+ const text = digestText(path.basename(repo), features, new Date(), Number(values.days) || 7, lessons);
101
+ console.log(text);
102
+ if (!values.notify) return 0;
103
+ if (config.notifiers.length === 0) {
104
+ console.error("\n没有配置外部通知(notifiers),只打印了总览。");
105
+ return 1;
106
+ }
107
+ let failed = false;
108
+ await dispatchNotifiers(
109
+ config.notifiers,
110
+ {
111
+ schema: 1,
112
+ type: "digest",
113
+ at: new Date().toISOString(),
114
+ kitVersion: kitVersion(),
115
+ feature: { dir: "docs/features", name: "", title: `${path.basename(repo)} 需求总览` },
116
+ repo,
117
+ branch: null,
118
+ profile: "",
119
+ originAgentId: null,
120
+ text,
121
+ details: {
122
+ features: features.map((f) => ({ name: f.name, title: f.title, stage: f.stage, branch: f.branch, updatedAt: f.updatedAt, usage: f.usage })),
123
+ recurringLessons: lessons,
124
+ },
125
+ },
126
+ (message) => {
127
+ if (message.startsWith("外部通知失败")) failed = true;
128
+ console.error(message);
129
+ },
130
+ );
131
+ return failed ? 1 : 0;
132
+ }
133
+
134
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
135
+ main().then(
136
+ (code) => process.exit(code),
137
+ (error: Error) => {
138
+ console.error(error.message);
139
+ process.exit(1);
140
+ },
141
+ );
142
+ }
@@ -0,0 +1,462 @@
1
+ // req-kit doctor: read-only health check, run by the /req-doctor skill.
2
+ //
3
+ // node bin/req-doctor.mjs [--repo <dir>] [--no-project] [--offline] [--config <file>]
4
+ //
5
+ // Sections: installation (delegated to install.mjs --check), configuration,
6
+ // Paseo daemon, and the current project with its pipelines.
7
+
8
+ import { execFileSync, spawnSync } from "node:child_process";
9
+ import { type PaseoJson, paseoJsonIssues } from "./paseo-json.js";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { parseArgs } from "node:util";
13
+ import { ConfigError, DEFAULT_CONFIG, loadConfig, type PipelineConfig, PROJECT_CONFIG_FILE, resolveRelatedRepo } from "./config.js";
14
+ import { checkProfile, connect, type Report, satisfies } from "./daemon-check.js";
15
+ import { daemonVersion } from "./daemon-version.js";
16
+ import { gitInfo } from "./git.js";
17
+ import { defaultConfigPath, findDependency, KIT_ROOT, kitVersion } from "./locations.js";
18
+ import { PROJECT_DIR } from "./prompts.js";
19
+ import { describeAgentProfile, ProfileResolutionError, profilesFromDaemonConfig, resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
20
+ import { isAlive, STATE_VERSION } from "./state.js";
21
+
22
+ const counts = { fail: 0, warn: 0 };
23
+ const SYMBOL = { ok: "✓", warn: "⚠", fail: "✗" } as const;
24
+
25
+ const report: Report = (level, text, fix) => {
26
+ if (level !== "ok") counts[level] += 1;
27
+ console.log(`${SYMBOL[level]} ${text}`);
28
+ if (fix) console.log(` 修复:${fix}`);
29
+ };
30
+
31
+ function section(title: string): void {
32
+ console.log(`\n## ${title}\n`);
33
+ }
34
+
35
+ /** Newer published version, as an informational line; never counted as a problem. */
36
+ function newVersion(): void {
37
+ const result = spawnSync(process.execPath, [path.join(KIT_ROOT, "install.mjs"), "--latest", "--quiet"], {
38
+ encoding: "utf8",
39
+ windowsHide: true,
40
+ timeout: 20_000,
41
+ });
42
+ const latest = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
43
+ const current = kitVersion();
44
+ if (!latest) {
45
+ console.log(`· 无法检查是否有新版本(${(result.stderr ?? "").trim().split("\n").at(-1) || "网络不通或超时"}),不影响使用`);
46
+ } else if (latest !== current && newer(latest, current)) {
47
+ console.log(`↑ 有新版本 ${latest}(当前 ${current})。升级:req upgrade(或在安装目录运行 node install.mjs --upgrade)`);
48
+ } else {
49
+ report("ok", `已是最新版本 ${current}`);
50
+ }
51
+ }
52
+
53
+ /** a > b in semver order (prereleases before their release). */
54
+ function newer(a: string, b: string): boolean {
55
+ const parse = (v: string) => {
56
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(v);
57
+ return m ? { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null } : null;
58
+ };
59
+ const pa = parse(a);
60
+ const pb = parse(b);
61
+ if (!pa || !pb) return false;
62
+ for (let i = 0; i < 3; i++) if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] > pb.nums[i];
63
+ if (pa.pre === pb.pre) return false;
64
+ if (pa.pre === null) return true;
65
+ if (pb.pre === null) return false;
66
+ return pa.pre.localeCompare(pb.pre, "en", { numeric: true }) > 0;
67
+ }
68
+
69
+ function installation(offline: boolean): void {
70
+ section("安装");
71
+ const result = spawnSync(process.execPath, [path.join(KIT_ROOT, "install.mjs"), "--check", "--quiet"], {
72
+ encoding: "utf8",
73
+ windowsHide: true,
74
+ });
75
+ const out = `${result.stdout ?? ""}${result.stderr ?? ""}`.trimEnd();
76
+ for (const line of out.split("\n")) {
77
+ if (line.startsWith("✗")) counts.fail += 1;
78
+ else if (line.startsWith("⚠")) counts.warn += 1;
79
+ if (line.trim()) console.log(line);
80
+ }
81
+ const pkg = path.join(findDependency("@getpaseo/client") ?? path.join(KIT_ROOT, "node_modules", "@getpaseo", "client"), "package.json");
82
+ const range = readPaseoRange();
83
+ if (fs.existsSync(pkg) && range) {
84
+ const sdk = (JSON.parse(fs.readFileSync(pkg, "utf8")) as { version: string }).version;
85
+ if (satisfies(sdk, range)) report("ok", `Paseo SDK ${sdk}(本版本支持 ${range})`);
86
+ else report("warn", `Paseo SDK ${sdk} 不在本版本支持的范围 ${range} 内`, "确认 Paseo 与 req-kit 版本匹配,必要时升级 req-kit");
87
+ }
88
+ if (!offline) newVersion();
89
+ }
90
+
91
+ function readPaseoRange(): string | null {
92
+ try {
93
+ const pkg = JSON.parse(fs.readFileSync(path.join(KIT_ROOT, "package.json"), "utf8")) as { reqKit?: { paseo?: string } };
94
+ return pkg.reqKit?.paseo ?? null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function configuration(configPath: string, repo: string | null): PipelineConfig | null {
101
+ section("配置");
102
+ if (!fs.existsSync(configPath)) {
103
+ report("fail", `找不到本机配置 ${configPath}`, "在 req-kit 安装目录运行 node install.mjs 生成,或用 node install.mjs --import-legacy <旧安装目录> 导入旧配置");
104
+ return null;
105
+ }
106
+ try {
107
+ const loaded = loadConfig(configPath, repo ?? undefined);
108
+ report("ok", `配置:${loaded.sources.join(" → ")}`);
109
+ if (loaded.ignored.length > 0) {
110
+ report("warn", `${loaded.ignored.join("、")} 只能在本机配置中设置,已被忽略`, "从团队或项目配置中删除这些字段");
111
+ }
112
+ const { builder, reviewer } = loaded.config.agents;
113
+ report("ok", `builder:${describeAgentProfile(builder)};reviewer:${describeAgentProfile(reviewer)}`);
114
+ report("ok", `默认档位:${loaded.config.defaultProfile}`);
115
+ const panel = loaded.config.reviewPanel;
116
+ if (panel.enabled) {
117
+ report("ok", `评审小组:${panel.members.length} 位成员,用于 ${panel.steps.join("、")}(${panel.profiles.join("、")}档,首轮)`);
118
+ if (panel.members.length === 1) report("warn", "评审小组只有 1 位成员,效果接近单人评审", "建议配置 2–3 位,使用不同厂商的模型");
119
+ }
120
+ const proposals = loaded.config.designProposals;
121
+ if (proposals.enabled) {
122
+ report("ok", `方案提案:${proposals.members.length || "builder 和 reviewer 各"} ${proposals.members.length ? "位成员" : "一份"},用于 ${proposals.profiles.join("、")}档`);
123
+ }
124
+ if (repo) {
125
+ const { checks, relatedRepos } = loaded.config;
126
+ if (checks.length > 0) {
127
+ report("ok", `质量门:${checks.length} 项检查(${checks.map((c) => c.name).join("、")}),流水线在写代码和修复的步骤之后自己运行`);
128
+ for (const c of checks) {
129
+ if (c.cwd && !fs.existsSync(path.join(repo, c.cwd))) report("warn", `检查「${c.name}」的目录 ${c.cwd} 不存在`, `修改 ${PROJECT_CONFIG_FILE} 中的 checks`);
130
+ }
131
+ } else {
132
+ report(
133
+ "warn",
134
+ "没有配置质量门的检查(checks):流水线不会自己运行 lint、测试等检查,只能依靠各步骤自己的记录",
135
+ "在项目根目录的对话中运行 /req-setup,它会把实际跑通的检查命令写进项目配置",
136
+ );
137
+ }
138
+ for (const r of relatedRepos) {
139
+ const dir = resolveRelatedRepo(r, repo);
140
+ if (fs.existsSync(dir)) report("ok", `关联仓库(只读参考):${r.name} → ${dir}`);
141
+ else report("warn", `关联仓库「${r.name}」不在这台机器上:${dir}`, "把它克隆到这个位置;或修改 relatedRepos 中的路径(相对项目目录,团队约定把相关仓库放在同一个上级目录最方便)");
142
+ }
143
+ }
144
+ const required = loaded.config.requiredVersion;
145
+ if (required) {
146
+ if (satisfies(kitVersion(), required)) report("ok", `版本要求 ${required}:当前 ${kitVersion()} 满足`);
147
+ else report("warn", `团队或项目要求 req-kit ${required},当前为 ${kitVersion()}`, "按 docs/guide/安装与升级.md 升级到符合要求的版本");
148
+ }
149
+ return loaded.config;
150
+ } catch (error) {
151
+ if (!(error instanceof ConfigError)) throw error;
152
+ report("fail", error.message, `编辑 ${configPath}`);
153
+ return null;
154
+ }
155
+ }
156
+
157
+ /** Daemon address from the raw file, so the daemon can be checked even when the agents are not filled in yet. */
158
+ function daemonSettings(configPath: string): PipelineConfig {
159
+ try {
160
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as Partial<PipelineConfig>;
161
+ return { ...DEFAULT_CONFIG, ...(raw.daemonUrl ? { daemonUrl: raw.daemonUrl } : {}), ...(raw.daemonPasswordEnv ? { daemonPasswordEnv: raw.daemonPasswordEnv } : {}) };
162
+ } catch {
163
+ return DEFAULT_CONFIG;
164
+ }
165
+ }
166
+
167
+ /** Informational line: never counted as a problem. */
168
+ function info(text: string): void {
169
+ console.log(`· ${text}`);
170
+ }
171
+
172
+ async function daemon(config: PipelineConfig | null, configPath: string): Promise<void> {
173
+ section("Paseo daemon");
174
+ const settings = config ?? daemonSettings(configPath);
175
+ let client;
176
+ try {
177
+ client = await connect(settings);
178
+ } catch (error) {
179
+ report(
180
+ "fail",
181
+ `无法连接 daemon ${settings.daemonUrl}:${(error as Error).message}`,
182
+ `确认 Paseo 桌面端或 daemon 正在运行;设置了密码时,确认环境变量 ${settings.daemonPasswordEnv} 已设置`,
183
+ );
184
+ return;
185
+ }
186
+ try {
187
+ report("ok", `已连接 ${settings.daemonUrl}`);
188
+ const range = readPaseoRange();
189
+ const version = await daemonVersion(settings.daemonUrl, process.env[settings.daemonPasswordEnv]).catch(() => null);
190
+ if (version && range) {
191
+ if (satisfies(version, range)) report("ok", `Paseo ${version}(本版本支持 ${range})`);
192
+ else report("warn", `Paseo ${version} 不在本版本 req-kit 支持的范围 ${range} 内`, "运行 req upgrade,它会选能支持这个 Paseo 版本的 req-kit");
193
+ }
194
+ const snapshot = await client.providers.waitForReady({ timeoutMs: 30_000 });
195
+ const providers = snapshot.entries.map((e) => `${e.provider}(${e.status})`).join("、");
196
+ report("ok", `可用的 provider:${providers || "(无)"}`);
197
+ const daemonConfig = await client.config.get().then((r) => r.config as Record<string, unknown>).catch(() => null);
198
+ const paseoProfiles = daemonConfig ? profilesFromDaemonConfig(daemonConfig) : null;
199
+ if (paseoProfiles && paseoProfiles.length > 0) {
200
+ report("ok", `Paseo agent profiles:${paseoProfiles.map((p) => `「${p.name}」`).join("、")}(配置中可以用 "paseoProfile": "<名称>" 引用)`);
201
+ } else if (paseoProfiles) {
202
+ info("Paseo 中还没有 agent profile。可选:在 设置 → Agents → Agent profiles 中建好\"实现\"\"评审\"等 profile,配置中用 paseoProfile 引用,之后换模型只需改 Paseo");
203
+ }
204
+ if (daemonConfig) {
205
+ const mcp = daemonConfig.mcp as { injectIntoAgents?: boolean } | undefined;
206
+ const browser = daemonConfig.browserTools as { enabled?: boolean } | undefined;
207
+ info(
208
+ mcp?.injectIntoAgents
209
+ ? "Paseo 工具已开启:步骤 agent 可以重启 Paseo 管理的服务"
210
+ : "Paseo 工具未开启(设置 → Agents → Enable Paseo tools)。不影响运行;开启后步骤 agent 可以自己重启 Paseo 管理的服务",
211
+ );
212
+ info(browser?.enabled ? "浏览器工具已开启(桌面端)" : "浏览器工具未开启(不影响运行)");
213
+ }
214
+ if (config && usesPaseoProfiles(config)) {
215
+ try {
216
+ config = resolveConfigProfiles(config, paseoProfiles);
217
+ report("ok", `Paseo profile 已解析:builder ${describeAgentProfile(config.agents.builder)};reviewer ${describeAgentProfile(config.agents.reviewer)}`);
218
+ } catch (error) {
219
+ if (!(error instanceof ProfileResolutionError)) throw error;
220
+ report("fail", error.message, "在 Paseo 中建好这个 profile,或修改配置中的 paseoProfile");
221
+ config = null;
222
+ }
223
+ }
224
+ if (config) {
225
+ const used = [...new Set([config.agents.builder, config.agents.reviewer].map((p) => (p.provider ?? "").split("/")[0]))];
226
+ const usage = await client.providers.listUsage().catch(() => null);
227
+ for (const p of usage?.providers ?? []) {
228
+ if (!used.includes(p.providerId) || p.status !== "available" || p.windows.length === 0) continue;
229
+ info(`${p.displayName} 额度:${p.windows.map((w) => `${w.label} 已用 ${typeof w.usedPct === "number" ? `${Math.round(w.usedPct)}%` : "?"}`).join(",")}`);
230
+ }
231
+ }
232
+ if (config) {
233
+ for (const role of ["builder", "reviewer"] as const) await checkProfile(client, role, config.agents[role], report);
234
+ if (config.reviewPanel.enabled) {
235
+ for (const [i, member] of config.reviewPanel.members.entries()) {
236
+ await checkProfile(client, `评审小组·${member.name ?? `成员 ${i + 1}`}`, member, report);
237
+ }
238
+ }
239
+ if (config.designProposals.enabled) {
240
+ for (const [i, member] of config.designProposals.members.entries()) {
241
+ await checkProfile(client, `方案提案·${member.name ?? `成员 ${i + 1}`}`, member, report);
242
+ }
243
+ }
244
+ } else {
245
+ report("warn", "配置不完整,跳过 agent 检查", "修好配置后再运行一次");
246
+ }
247
+ } catch (error) {
248
+ report("fail", `检查 daemon 时出错:${(error as Error).message}`);
249
+ } finally {
250
+ await client.close().catch(() => undefined);
251
+ }
252
+ }
253
+
254
+ /** Files whose change usually means the build, test or start commands changed. */
255
+ const BUILD_FILES = [
256
+ "package.json",
257
+ "pnpm-workspace.yaml",
258
+ "Makefile",
259
+ "justfile",
260
+ "docker-compose.yml",
261
+ "compose.yaml",
262
+ "pyproject.toml",
263
+ "go.mod",
264
+ "pom.xml",
265
+ "build.gradle",
266
+ "build.gradle.kts",
267
+ "Cargo.toml",
268
+ ];
269
+
270
+ function lastCommitTime(repo: string, files: string[]): number | null {
271
+ try {
272
+ const out = execFileSync("git", ["log", "-1", "--format=%ct", "--", ...files], {
273
+ cwd: repo,
274
+ encoding: "utf8",
275
+ stdio: ["ignore", "pipe", "ignore"],
276
+ }).trim();
277
+ return out ? Number(out) * 1000 : null;
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
283
+ function verifyInstructions(repo: string, isRepo: boolean): void {
284
+ const rel = `${PROJECT_DIR}/verify.md`;
285
+ const file = path.join(repo, PROJECT_DIR, "verify.md");
286
+ if (!fs.existsSync(file)) {
287
+ report("warn", "没有验证方法说明,服务启动和功能验证步骤需要自己摸索,耗时更长、结果更不稳定", "在项目根目录的对话中运行 /req-setup");
288
+ return;
289
+ }
290
+ if (!isRepo) {
291
+ report("ok", `验证方法:${rel}`);
292
+ return;
293
+ }
294
+ const verifyTime = lastCommitTime(repo, [path.join(PROJECT_DIR, "verify.md")]);
295
+ const buildFiles = BUILD_FILES.filter((f) => fs.existsSync(path.join(repo, f)));
296
+ const buildTime = buildFiles.length > 0 ? lastCommitTime(repo, buildFiles) : null;
297
+ if (verifyTime === null) {
298
+ report("warn", `验证方法 ${rel} 还没有提交,其他工作区看不到它`, "确认内容后提交,并合并到主分支");
299
+ } else if (buildTime !== null && buildTime > verifyTime) {
300
+ const day = (ms: number) => new Date(ms).toLocaleDateString("zh-CN");
301
+ report("warn", `验证方法可能过时:构建文件在 ${day(buildTime)} 改过,验证方法最后更新于 ${day(verifyTime)}`, "运行 /req-setup 更新,或确认内容仍然准确");
302
+ } else {
303
+ report("ok", `验证方法:${rel}`);
304
+ }
305
+ }
306
+
307
+ interface RawState {
308
+ version?: number;
309
+ status?: string;
310
+ pid?: number;
311
+ stopReason?: string | null;
312
+ }
313
+
314
+ /** paseo.json: worktree setup and the services Paseo can run for the pipeline. */
315
+ function paseoJson(repo: string, isRepo: boolean): void {
316
+ const file = path.join(repo, "paseo.json");
317
+ if (!fs.existsSync(file)) {
318
+ info("没有 paseo.json。可选:/req-setup 可以生成它,让 Paseo 在新建 worktree 时自动安装依赖,并由 Paseo 管理服务和端口(几个需求同时验证也不会端口冲突)");
319
+ return;
320
+ }
321
+ let raw: PaseoJson;
322
+ try {
323
+ raw = JSON.parse(fs.readFileSync(file, "utf8")) as typeof raw;
324
+ } catch (error) {
325
+ report("fail", `paseo.json 不是合法的 JSON:${(error as Error).message}`, "修正 paseo.json");
326
+ return;
327
+ }
328
+ const scripts = Object.entries(raw.scripts ?? {});
329
+ const services = scripts.filter(([, s]) => s.type === "service");
330
+ report(
331
+ "ok",
332
+ `paseo.json:${raw.worktree?.setup ? "新建 worktree 时自动准备环境;" : ""}脚本 ${scripts.length} 个${services.length ? `,其中服务 ${services.map(([n]) => n).join("、")}(由流水线通过 Paseo 启动)` : ""}`,
333
+ );
334
+ for (const [name, s] of services) {
335
+ if (s.port === undefined && !/PASEO_PORT|\{PORT\}|with-port/.test(String(s.command ?? ""))) {
336
+ report("warn", `paseo.json 中的服务 ${name} 没有使用 Paseo 分配的端口(PASEO_PORT),几个工作区同时运行时会端口冲突`, `让服务监听 Paseo 分配的端口,例如 "node scripts/paseo/with-port.mjs npm run dev -- --port {PORT}"(/req-setup 会写入这个脚本)`);
337
+ }
338
+ }
339
+ for (const issue of paseoJsonIssues(raw)) {
340
+ if (issue.kind === "fails") report("warn", issue.text, issue.fix);
341
+ else info(`${issue.text}。团队都用同一种系统时可以不改;否则:${issue.fix}`);
342
+ }
343
+ if (!isRepo) return;
344
+ try {
345
+ execFileSync("git", ["ls-files", "--error-unmatch", "paseo.json"], { cwd: repo, stdio: "ignore" });
346
+ } catch {
347
+ report("warn", "paseo.json 还没有提交:Paseo 新建 worktree 时读取的是已提交的版本", "提交 paseo.json");
348
+ }
349
+ }
350
+
351
+ function project(repo: string): void {
352
+ section(`项目:${repo}`);
353
+ const git = gitInfo(repo);
354
+ if (!git.isRepo) {
355
+ report("warn", "不是 git 仓库:流水线可以运行,但不会提交代码");
356
+ } else if (git.detached) {
357
+ report("warn", "当前没有处在任何分支上(detached HEAD),流水线会拒绝启动", "切换到需求的工作分支");
358
+ } else if (git.branch && git.protectedBranches.includes(git.branch)) {
359
+ report("warn", `当前在默认分支 ${git.branch} 上:新需求会被建议改用独立的 worktree 工作区`, "在 Paseo 中 New workspace → Isolation 选 New worktree");
360
+ } else {
361
+ report("ok", `当前分支:${git.branch}`);
362
+ }
363
+ const projectConfig = path.join(repo, PROJECT_CONFIG_FILE);
364
+ if (fs.existsSync(projectConfig)) report("ok", `项目配置:${PROJECT_CONFIG_FILE}`);
365
+ const notes = path.join(repo, PROJECT_DIR, "project-notes.md");
366
+ if (fs.existsSync(notes)) report("ok", `项目说明:${PROJECT_DIR}/project-notes.md`);
367
+ else report("warn", "没有项目说明(包管理工具、不能改的目录、提交格式等),步骤 agent 需要自己摸索", "在项目根目录的对话中运行 /req-setup");
368
+ verifyInstructions(repo, git.isRepo);
369
+ paseoJson(repo, git.isRepo);
370
+ if (!fs.existsSync(path.join(repo, "GLOSSARY.md")) && fs.existsSync(path.join(repo, "CONTEXT.md"))) {
371
+ report("ok", "术语表为旧名 CONTEXT.md:会继续读取,新术语写入 GLOSSARY.md");
372
+ } else if (fs.existsSync(path.join(repo, "GLOSSARY.md"))) {
373
+ report("ok", "术语表:GLOSSARY.md");
374
+ } else {
375
+ report("warn", "没有术语表 GLOSSARY.md:需求方的说法和代码里的叫法对不上时容易做偏", "在项目根目录的对话中运行 /req-setup,它会整理主要的业务词和代码中的名字");
376
+ }
377
+
378
+ const featuresDir = path.join(repo, "docs", "features");
379
+ if (!fs.existsSync(featuresDir)) {
380
+ report("ok", "还没有需求(docs/features 不存在)");
381
+ return;
382
+ }
383
+ let found = 0;
384
+ for (const name of fs.readdirSync(featuresDir).sort()) {
385
+ const file = path.join(featuresDir, name, ".pipeline", "state.json");
386
+ if (!fs.existsSync(file)) continue;
387
+ found += 1;
388
+ let raw: RawState;
389
+ try {
390
+ raw = JSON.parse(fs.readFileSync(file, "utf8")) as RawState;
391
+ } catch (error) {
392
+ report("fail", `${name}:状态文件无法读取:${(error as Error).message}`, `检查 ${file}`);
393
+ continue;
394
+ }
395
+ const version = typeof raw.version === "number" ? raw.version : 1;
396
+ if (version > STATE_VERSION) {
397
+ report("fail", `${name}:状态文件由更新版本的 req-kit 写入(第 ${version} 版)`, "升级 req-kit,或恢复同目录下的 state.json.bak-v* 备份");
398
+ continue;
399
+ }
400
+ const migrate = version < STATE_VERSION ? `(格式第 ${version} 版,下次继续时会自动迁移并保留备份)` : "";
401
+ const label = `docs/features/${name}`;
402
+ switch (raw.status) {
403
+ case "running":
404
+ if (raw.pid && isAlive(raw.pid)) report("ok", `${label}:运行中(进程 ${raw.pid})${migrate}`);
405
+ else report("warn", `${label}:状态为运行中,但进程已不在${migrate}`, `在该需求的对话中对 /req 说"继续开发",或执行 start-pipeline --feature ${label} --resume`);
406
+ break;
407
+ case "waiting_for_approval":
408
+ report("warn", `${label}:方案等待需求方确认${migrate}`, "在该需求的对话中看 preview.md 并确认(或修改需求)");
409
+ break;
410
+ case "waiting_for_business":
411
+ report("warn", `${label}:等待需求方决定${migrate}`, "在该需求的对话中回答问题、更新 spec 后继续");
412
+ break;
413
+ case "failed":
414
+ report("warn", `${label}:已中断:${raw.stopReason ?? "原因未记录"}${migrate}`, `查看 ${label}/.pipeline/pipeline.log,解决后继续`);
415
+ break;
416
+ case "completed":
417
+ report("ok", `${label}:已完成${migrate}`);
418
+ break;
419
+ default:
420
+ report("warn", `${label}:未知状态 ${raw.status}`);
421
+ }
422
+ }
423
+ if (found === 0) report("ok", "没有运行过流水线的需求");
424
+ }
425
+
426
+ async function main(): Promise<number> {
427
+ const { values } = parseArgs({
428
+ args: process.argv.slice(2),
429
+ options: {
430
+ repo: { type: "string" },
431
+ config: { type: "string" },
432
+ "no-project": { type: "boolean", default: false },
433
+ offline: { type: "boolean", default: false },
434
+ },
435
+ });
436
+ const configPath = values.config ? path.resolve(values.config) : defaultConfigPath();
437
+ const repo = values["no-project"] ? null : path.resolve(values.repo ?? process.env.PASEO_AGENT_CWD ?? process.cwd());
438
+
439
+ console.log(`req-kit ${kitVersion()} 体检(安装目录 ${KIT_ROOT})`);
440
+ installation(values.offline ?? false);
441
+ const config = configuration(configPath, repo);
442
+ if (values.offline) {
443
+ section("Paseo daemon");
444
+ console.log("(已跳过:--offline)");
445
+ } else {
446
+ await daemon(config, configPath);
447
+ }
448
+ if (repo) project(repo);
449
+
450
+ console.log("");
451
+ if (counts.fail === 0 && counts.warn === 0) console.log("结论:全部正常。");
452
+ else console.log(`结论:${counts.fail} 项必须处理(✗),${counts.warn} 项建议处理(⚠)。`);
453
+ return counts.fail === 0 ? 0 : 1;
454
+ }
455
+
456
+ main().then(
457
+ (code) => process.exit(code),
458
+ (error: Error) => {
459
+ console.error(`体检出错:${error.message}`);
460
+ process.exit(2);
461
+ },
462
+ );