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,46 @@
1
+ // Requirement types with their own playbook (spec "- 类型:"). A playbook keeps the
2
+ // pipeline's steps but changes what some of them do, through a prompt file with one
3
+ // section per step, and the flow skips what the type does not need. See ADR 0023.
4
+ //
5
+ // 缺陷 (defect) design becomes "reproduce and locate": a failing reproduction first;
6
+ // cannot reproduce → stop and ask. The fix must turn it green.
7
+ // 重构 (refactor) behavior is locked first (characterization tests), then small steps,
8
+ // each keeping every check green; review checks that behavior did not change.
9
+ // 评估 (assessment) read-only: research → assessment.md → independent review → report. No code.
10
+
11
+ import type { RequirementType } from "./config.js";
12
+ import { type PromptSources, readToolPrompt } from "./prompts.js";
13
+ import { STEPS, type StepId } from "./steps.js";
14
+
15
+ const TITLES: Record<RequirementType, Partial<Record<StepId, string>>> = {
16
+ defect: { design: "复现与定位" },
17
+ refactor: { design: "方案设计与行为锁定" },
18
+ assessment: { design: "调研评估", design_review: "评估审阅", design_revise: "评估修订", report: "评估报告" },
19
+ };
20
+
21
+ /** The step's title for this requirement type. */
22
+ export function stepTitle(type: RequirementType | null | undefined, id: StepId): string {
23
+ return (type && TITLES[type][id]) || STEPS[id].title;
24
+ }
25
+
26
+ /**
27
+ * The playbook's guidance for one step: the "## 所有步骤" section plus the section whose
28
+ * heading names the step id in parentheses, e.g. "## 代码实现(implement)". Null when the
29
+ * playbook has nothing for this step.
30
+ */
31
+ export function playbookSection(sources: PromptSources, type: RequirementType, id: StepId): string | null {
32
+ return promptSections(sources, `types/${type}.md`, id);
33
+ }
34
+
35
+ /** The same section picking for any prompt file written that way (e.g. joint.md). */
36
+ export function promptSections(sources: PromptSources, file: string, id: StepId): string | null {
37
+ const text = readToolPrompt(sources, file);
38
+ const [intro, ...sections] = text.split(/^## /m);
39
+ const picked = sections.filter((s) => {
40
+ const heading = s.split("\n", 1)[0];
41
+ return heading.startsWith("所有步骤") || heading.includes(`(${id})`) || heading.includes(`(${id})`);
42
+ });
43
+ // The common part alone is still worth giving: the type changes what every step looks for.
44
+ if (picked.length === 0) return null;
45
+ return [intro.trim(), ...picked.map((s) => `## ${s.trim()}`)].join("\n\n");
46
+ }
@@ -0,0 +1,337 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ type DefectHandling,
6
+ PHASE_MODE_LABELS,
7
+ PHASE_MODES,
8
+ type PhaseMode,
9
+ PROFILE_LABELS,
10
+ PROFILE_NAMES,
11
+ type ProfileName,
12
+ type RequirementType,
13
+ type VerifyEnvironment,
14
+ } from "./config.js";
15
+ import { gitInfo } from "./git.js";
16
+ import { samePath } from "./paths.js";
17
+ import { currentLockHolder, type PipelineState, readState, statePath } from "./state.js";
18
+
19
+ export interface LaunchArgs {
20
+ repo: string;
21
+ featureDir: string;
22
+ originAgentId: string | null;
23
+ resume: boolean;
24
+ restart: boolean;
25
+ configPath: string | null;
26
+ /** Explicit --profile; otherwise the spec's "档位" line, then the configured default. */
27
+ profile: ProfileName | null;
28
+ status: boolean;
29
+ stop: boolean;
30
+ /** --list: print every requirement in this repository's worktrees. */
31
+ list: boolean;
32
+ /** --spec-review: independent review of the draft spec, before confirmation. */
33
+ specReview: boolean;
34
+ /** --worktree: create a Paseo worktree workspace for this requirement and develop there. */
35
+ worktree: boolean;
36
+ /**
37
+ * The checkout the chat works in, when the pipeline runs in a worktree created for it
38
+ * (repo is then that worktree). Set by the launcher; the chat's cwd must be this.
39
+ */
40
+ sourceRepo: string | null;
41
+ /** Paseo workspace of that worktree. */
42
+ workspaceId: string | null;
43
+ /** --at: schedule the start for later instead of starting now. */
44
+ at: string | null;
45
+ /** --phases: execution mode for this run; otherwise the spec's "执行方式" line, then the config. */
46
+ phases: PhaseMode | null;
47
+ /** --phase verify: start (or continue) the verify phase. Implies --resume. */
48
+ phase: "verify" | null;
49
+ /** --verify-at: once development is done, start the verify phase at this time. */
50
+ verifyAt: string | null;
51
+ /** --verify-env: where the verify phase checks the result (overrides phases.verify.environment). */
52
+ verifyEnv: VerifyEnvironment | null;
53
+ /** --defects: what the verify phase does with defects (overrides phases.verify.defects). */
54
+ defects: DefectHandling | null;
55
+ /** --approve: the requester confirmed the design waiting at the confirmation point. Implies --resume. */
56
+ approve: boolean;
57
+ }
58
+
59
+ export class PreflightError extends Error {
60
+ constructor(message: string) {
61
+ super(message);
62
+ this.name = "PreflightError";
63
+ }
64
+ }
65
+
66
+ function gitCommonDir(dir: string): string | null {
67
+ try {
68
+ const out = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
69
+ cwd: dir,
70
+ encoding: "utf8",
71
+ stdio: ["ignore", "pipe", "ignore"],
72
+ }).trim();
73
+ return out ? fs.realpathSync(out) : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /** Whether `worktree` is a git worktree of the same repository as `source`. */
80
+ export function sameRepository(worktree: string, source: string): boolean {
81
+ const a = gitCommonDir(worktree);
82
+ const b = gitCommonDir(source);
83
+ return a !== null && b !== null && samePath(a, b);
84
+ }
85
+
86
+ /**
87
+ * Step agents run under the launching chat. Normally they work in the chat's own
88
+ * directory, so the repository must be it. A requirement developed in a worktree
89
+ * created for it (--worktree) runs in that worktree instead; the chat must then be
90
+ * in the checkout the worktree was created from.
91
+ */
92
+ export function checkChatCwd(args: Pick<LaunchArgs, "originAgentId" | "sourceRepo">, repo: string, env: NodeJS.ProcessEnv = process.env): void {
93
+ const chatCwd = env.PASEO_AGENT_CWD?.trim();
94
+ if (!args.originAgentId || !chatCwd || env.PASEO_AGENT_ID?.trim() !== args.originAgentId) return;
95
+ const chat = fs.existsSync(chatCwd) ? fs.realpathSync(chatCwd) : chatCwd;
96
+ if (args.sourceRepo) {
97
+ const source = fs.existsSync(args.sourceRepo) ? fs.realpathSync(args.sourceRepo) : args.sourceRepo;
98
+ if (samePath(chat, source) && sameRepository(repo, source)) return;
99
+ throw new PreflightError(
100
+ `这个需求在为它新建的工作区 ${repo} 中开发,只能从 ${source} 的对话中操作,当前对话的工作目录是 ${chatCwd}。`,
101
+ );
102
+ }
103
+ if (!samePath(chat, repo)) {
104
+ throw new PreflightError(
105
+ `仓库目录 ${repo} 与当前对话的工作目录 ${chatCwd} 不一致。` +
106
+ "流水线的步骤会挂在这个对话下、在对话的工作目录中进行,所以只能为对话所在的项目启动。" +
107
+ "请在该仓库对应的项目中打开对话后再启动;对话过程中不要切换工作目录。",
108
+ );
109
+ }
110
+ }
111
+
112
+ /** The spec must carry the line written by the kickoff skill after the user confirms it. */
113
+ export function isSpecConfirmed(specText: string): boolean {
114
+ return /^\s*-\s*状态\s*[::]\s*已确认/m.test(specText);
115
+ }
116
+
117
+ export function normalizeFeatureDir(repo: string, featureDir: string): string {
118
+ const absolute = path.resolve(repo, featureDir);
119
+ const relative = path.relative(repo, absolute);
120
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
121
+ throw new PreflightError(`功能目录必须位于仓库目录之内:${featureDir}`);
122
+ }
123
+ return relative.split(path.sep).join("/");
124
+ }
125
+
126
+ export type StartMode =
127
+ | { kind: "fresh" }
128
+ | { kind: "restart"; previous: PipelineState }
129
+ | { kind: "resume-reuse"; previous: PipelineState }
130
+ | { kind: "resume-from-design"; previous: PipelineState; reason: string };
131
+
132
+ export interface PreflightResult {
133
+ repo: string;
134
+ featureDir: string;
135
+ specPath: string;
136
+ mode: StartMode;
137
+ /** Branch the run will commit to; null outside a git repository. */
138
+ branch: string | null;
139
+ /** Profile named in the spec, if any. */
140
+ specProfile: ProfileName | null;
141
+ /** Execution mode named in the spec, if any. */
142
+ specPhases: PhaseMode | null;
143
+ /** Requirement type with its own playbook named in the spec, if any. */
144
+ specType: RequirementType | null;
145
+ /** Related repositories the spec says this requirement also changes. */
146
+ specJoint: string[];
147
+ /** This start begins the verify phase (the develop phase is done). */
148
+ entersVerify: boolean;
149
+ }
150
+
151
+ /** Reads "- 执行方式:分阶段" (连续 / 分阶段 / 只做研发, or the English names) from the spec header. */
152
+ export function specPhases(specText: string): PhaseMode | null {
153
+ const match = /^\s*-\s*执行方式\s*[::]\s*(\S+)/m.exec(specText);
154
+ if (!match) return null;
155
+ for (const mode of PHASE_MODES) {
156
+ if (match[1].startsWith(PHASE_MODE_LABELS[mode]) || match[1].toLowerCase().startsWith(mode)) return mode;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ const TYPE_WORDS: Array<[RequirementType, RegExp]> = [
162
+ ["defect", /^(缺陷|bug|defect|fix|问题修复)/i],
163
+ ["refactor", /^(重构|技术改进|refactor)/i],
164
+ ["assessment", /^(评估|调研|assessment|spike)/i],
165
+ ];
166
+
167
+ /** Reads "- 类型:缺陷" (缺陷 / 重构 / 评估, or the English names); 新需求 and 小改动 give null. */
168
+ export function specType(specText: string): RequirementType | null {
169
+ const match = /^\s*-\s*类型\s*[::]\s*(\S+)/m.exec(specText);
170
+ if (!match) return null;
171
+ return TYPE_WORDS.find(([, re]) => re.test(match[1]))?.[0] ?? null;
172
+ }
173
+
174
+ /** Reads "- 联合开发:后端、支付" (related repositories this requirement also changes). */
175
+ export function specJoint(specText: string): string[] {
176
+ const match = /^\s*-\s*联合开发\s*[::]\s*(.+)$/m.exec(specText);
177
+ if (!match) return [];
178
+ const names = match[1].split(/[、,,;;\s]+/).map((n) => n.replace(/[((].*$/, "").trim()).filter((n) => n && n !== "无");
179
+ return [...new Set(names)];
180
+ }
181
+
182
+ /** Whether a run has finished its develop phase, so the verify phase can start. */
183
+ export function developDone(state: PipelineState): boolean {
184
+ return state.status === "phase_done" || (state.status === "completed" && state.phases?.mode === "develop-only");
185
+ }
186
+
187
+ /** Reads "- 档位:快速" / "- 档位:标准" from the spec header. */
188
+ export function specProfile(specText: string): ProfileName | null {
189
+ const match = /^\s*-\s*档位\s*[::]\s*(\S+)/m.exec(specText);
190
+ if (!match) return null;
191
+ for (const name of PROFILE_NAMES) {
192
+ if (match[1].startsWith(PROFILE_LABELS[name]) || match[1].toLowerCase().startsWith(name)) return name;
193
+ }
194
+ return null;
195
+ }
196
+
197
+ /**
198
+ * Checks everything that can be checked without talking to the daemon.
199
+ * The launcher runs this before detaching so the chat agent gets an immediate,
200
+ * readable error; the pipeline process runs it again before taking the lock.
201
+ */
202
+ export function preflight(
203
+ args: LaunchArgs,
204
+ currentSpecHash: (file: string) => string,
205
+ env: NodeJS.ProcessEnv = process.env,
206
+ ): PreflightResult {
207
+ if (!fs.existsSync(args.repo) || !fs.statSync(args.repo).isDirectory()) {
208
+ throw new PreflightError(`仓库目录不存在:${args.repo}`);
209
+ }
210
+ const repo = fs.realpathSync(args.repo);
211
+ checkChatCwd(args, repo, env);
212
+ const featureDir = normalizeFeatureDir(repo, args.featureDir);
213
+ const featureAbs = path.join(repo, featureDir);
214
+ if (!fs.existsSync(featureAbs) || !fs.statSync(featureAbs).isDirectory()) {
215
+ throw new PreflightError(`功能目录不存在:${featureDir}`);
216
+ }
217
+ const specPath = path.join(featureAbs, "spec.md");
218
+ if (!fs.existsSync(specPath)) {
219
+ throw new PreflightError(`功能目录中没有 spec.md:${featureDir}`);
220
+ }
221
+ const specText = fs.readFileSync(specPath, "utf8");
222
+ if (!isSpecConfirmed(specText)) {
223
+ throw new PreflightError(
224
+ 'spec.md 还没有被确认(顶部需要有 "- 状态:已确认" 这一行)。请先和用户确认需求说明。',
225
+ );
226
+ }
227
+ if (args.resume && args.restart) {
228
+ throw new PreflightError("--resume 和 --restart 不能同时使用");
229
+ }
230
+
231
+ const holder = currentLockHolder(repo);
232
+ if (holder) {
233
+ throw new PreflightError(
234
+ `这个仓库目录已有一条流水线在运行(进程 ${holder.pid},功能目录 ${holder.featureDir})。同一个目录一次只能运行一条。`,
235
+ );
236
+ }
237
+
238
+ const previous = readState(statePath(repo, featureDir));
239
+ let mode: StartMode;
240
+ if (!previous) {
241
+ if (args.resume) throw new PreflightError("没有找到之前的运行记录,无法继续。去掉 --resume 重新启动即可。");
242
+ mode = { kind: "fresh" };
243
+ } else if (args.restart) {
244
+ mode = { kind: "restart", previous };
245
+ } else if (!args.resume) {
246
+ const hint =
247
+ previous.status === "phase_done"
248
+ ? "这个需求的研发阶段已经完成,正在等待测试验证。开始测试验证请加 --resume --phase verify(或只加 --resume);要从头开始请加 --restart。"
249
+ : previous.status === "completed"
250
+ ? "这个需求的流水线已经完成。如需重新完整运行,请加 --restart。"
251
+ : `这个需求已有运行记录(状态:${previous.status})。从中断处继续请加 --resume,从头开始请加 --restart。`;
252
+ throw new PreflightError(hint);
253
+ } else if (args.approve && previous.status !== "waiting_for_approval") {
254
+ throw new PreflightError(`这个需求没有等待确认的方案(状态:${previous.status}),不需要 --approve。继续开发请用 --resume。`);
255
+ } else if (previous.status === "waiting_for_approval" && currentSpecHash(specPath) !== previous.specHash) {
256
+ mode = { kind: "resume-from-design", previous, reason: "方案确认时需求方提出了修改,需求说明已更新,从方案设计开始增量处理" };
257
+ } else if (previous.status === "waiting_for_approval") {
258
+ if (!args.approve) {
259
+ throw new PreflightError(
260
+ "方案正在等待需求方确认(见功能目录中的 preview.md)。需求方确认后用 --approve 继续开发;要修改的话,先更新并重新确认需求说明,再用 --resume。",
261
+ );
262
+ }
263
+ mode = { kind: "resume-reuse", previous };
264
+ } else if (args.phase === "verify" && !developDone(previous) && previous.phase !== "verify") {
265
+ throw new PreflightError(
266
+ previous.status === "completed"
267
+ ? "这个需求已经完整走完了研发和测试验证,没有单独的测试阶段可以开始。要重新验证,请更新需求说明后用 --resume,或用 --restart。"
268
+ : `这个需求的研发阶段还没有完成(状态:${previous.status}),还不能开始测试验证。请先用 --resume 继续研发。`,
269
+ );
270
+ } else if (args.phase === "verify" && developDone(previous) && currentSpecHash(specPath) === previous.specHash) {
271
+ // Development is done and the requirement has not changed since: the verify phase starts.
272
+ mode = { kind: "resume-reuse", previous };
273
+ } else if (previous.status === "phase_done" && currentSpecHash(specPath) === previous.specHash) {
274
+ // A plain --resume at the gate starts the verify phase too.
275
+ mode = { kind: "resume-reuse", previous };
276
+ } else if (previous.status === "completed") {
277
+ // A completed requirement is reopened by changing its spec (a requirement change, or
278
+ // problems found during acceptance). Without such a change there is nothing to continue.
279
+ if (currentSpecHash(specPath) === previous.specHash) {
280
+ if (previous.phases?.mode === "develop-only") {
281
+ throw new PreflightError(
282
+ "这个需求按\"只做研发\"完成了研发,没有可继续的步骤。要让流水线做测试验证,请用 --resume --phase verify;要修改需求,请先更新并重新确认 spec.md。",
283
+ );
284
+ }
285
+ throw new PreflightError(
286
+ "这个需求的流水线已经完成,没有可继续的步骤。要修改需求或修复验收中发现的问题,请先更新并重新确认 spec.md;要原样重跑一遍,请用 --restart。",
287
+ );
288
+ }
289
+ mode = { kind: "resume-from-design", previous, reason: "流水线完成后 spec.md 被修改过(需求变更或验收发现的问题),从方案设计开始增量处理" };
290
+ } else if (previous.status === "waiting_for_business") {
291
+ mode = { kind: "resume-from-design", previous, reason: "上次因等待需求方决定而停止,需求可能已更新,从方案设计开始增量处理" };
292
+ } else if (currentSpecHash(specPath) !== previous.specHash) {
293
+ mode = { kind: "resume-from-design", previous, reason: "spec.md 在上次运行后被修改过,从方案设计开始增量处理" };
294
+ } else {
295
+ // "failed", or "running" left behind by a process that died without cleanup.
296
+ mode = { kind: "resume-reuse", previous };
297
+ }
298
+ const git = gitInfo(repo);
299
+ if (args.worktree) {
300
+ // The run will happen in a new worktree on its own branch, cut from what is checked out here.
301
+ if (mode.kind !== "fresh") {
302
+ throw new PreflightError("这个需求已经在这里运行过,不能再改为在新工作区中开发。请用 --resume 继续,或去掉 --worktree。");
303
+ }
304
+ if (!git.isRepo) throw new PreflightError("--worktree 需要 git 仓库。");
305
+ return { repo, featureDir, specPath, mode, branch: git.branch, specProfile: specProfile(specText), specPhases: specPhases(specText), specType: specType(specText), specJoint: specJoint(specText), entersVerify: false };
306
+ }
307
+ // The run commits to the branch that is checked out now, and never to the default branch.
308
+ if (git.isRepo && git.detached) {
309
+ throw new PreflightError("当前没有处在任何分支上(detached HEAD)。请切换到这个需求的工作分支后再启动。");
310
+ }
311
+ if (git.branch && git.protectedBranches.includes(git.branch)) {
312
+ throw new PreflightError(
313
+ `当前在默认分支 ${git.branch} 上,流水线不会在默认分支上提交代码。` +
314
+ "可以加 --worktree,让流水线为这个需求新建一个 worktree 工作区并在那里开发(这个对话不用动);" +
315
+ "或者在 Paseo 中自己新建一个 worktree 工作区(New workspace → Isolation 选 New worktree),在那里的对话中重新开始;" +
316
+ "或者先切换到这个需求的功能分支。",
317
+ );
318
+ }
319
+ if (mode.kind === "resume-reuse" && mode.previous.branch && git.branch !== mode.previous.branch) {
320
+ throw new PreflightError(
321
+ `这个需求之前在分支 ${mode.previous.branch} 上进行,现在的分支是 ${git.branch ?? "(无)"}。请切回原分支后继续,或用 --restart 在当前分支上从头开始。`,
322
+ );
323
+ }
324
+ const entersVerify = mode.kind === "resume-reuse" && developDone(mode.previous);
325
+ return { repo, featureDir, specPath, mode, branch: git.branch, specProfile: specProfile(specText), specPhases: specPhases(specText), specType: specType(specText), specJoint: specJoint(specText), entersVerify };
326
+ }
327
+
328
+ /** Why the verify phase cannot start in this environment, or null. */
329
+ export function verifyPhaseProblem(repo: string, featureDir: string, environment: VerifyEnvironment): string | null {
330
+ if (environment !== "deployed") return null;
331
+ if (fs.existsSync(path.join(repo, featureDir, "test-env.md"))) return null;
332
+ return (
333
+ `要在已部署的测试环境中验证,但功能目录中还没有 test-env.md(${featureDir}/test-env.md)。` +
334
+ "请先和用户确认测试环境,写好这个文件(地址、账号从哪里取、可以用的测试数据和账号;只写口令在哪里取,不写口令本身),再开始测试验证;" +
335
+ "或者用 --verify-env local 在本地验证。"
336
+ );
337
+ }
@@ -0,0 +1,72 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ // Prompt sources, from least to most specific:
5
+ // <kit>/engine/prompts/<file> shipped with this version
6
+ // <kit home>/prompts.local/<file> machine-wide override; replaces the shipped file, survives upgrades
7
+ // <kit>/principles/INDEX.md principles, also read by the chat skills
8
+ // <repo>/.requirement-pipeline/project-notes.md appended to every step of this project
9
+ // <repo>/.requirement-pipeline/verify.md appended to the steps that build, start or test the project
10
+ // <repo>/.requirement-pipeline/steps/<step>.md appended to one step of this project
11
+
12
+ export const PROJECT_DIR = ".requirement-pipeline";
13
+
14
+ export interface PromptLocations {
15
+ promptsDir: string;
16
+ localPromptsDir: string;
17
+ principlesFile: string;
18
+ }
19
+
20
+ export interface PromptSources extends PromptLocations {
21
+ repo: string;
22
+ }
23
+
24
+ export function readToolPrompt(sources: PromptSources, file: string): string {
25
+ const local = path.join(sources.localPromptsDir, file);
26
+ const shipped = path.join(sources.promptsDir, file);
27
+ return fs.readFileSync(fs.existsSync(local) ? local : shipped, "utf8").trim();
28
+ }
29
+
30
+ /** The shared principles, as a prompt section, or null when the index is missing. */
31
+ export function principlesSection(sources: PromptSources): string | null {
32
+ const text = readIfPresent(sources.principlesFile);
33
+ if (!text) return null;
34
+ // The index starts with its own title; give it a heading that fits inside the prompt.
35
+ const body = text.replace(/^#\s+.*\n+/, "");
36
+ return ["# 工作原则", "", "以下原则在需求对齐阶段和流水线各步骤中通用。具体要求以前文和\"本步骤任务\"为准,这里说明背后的取舍。", "", body].join("\n");
37
+ }
38
+
39
+ function readIfPresent(file: string): string | null {
40
+ if (!fs.existsSync(file)) return null;
41
+ const text = fs.readFileSync(file, "utf8").trim();
42
+ return text === "" ? null : text;
43
+ }
44
+
45
+ /** Steps that build, start or test the project, and so need the project's verification instructions. */
46
+ export const VERIFY_STEPS: readonly string[] = ["implement", "review_fix", "service_start", "verify", "defect_fix", "reverify"];
47
+
48
+ /** Project-specific additions for one step, ready to append, or null when there are none. */
49
+ export function projectAdditions(sources: PromptSources, stepId: string): string | null {
50
+ const notes = readIfPresent(path.join(sources.repo, PROJECT_DIR, "project-notes.md"));
51
+ const verify = VERIFY_STEPS.includes(stepId) ? readIfPresent(path.join(sources.repo, PROJECT_DIR, "verify.md")) : null;
52
+ const step = readIfPresent(path.join(sources.repo, PROJECT_DIR, "steps", `${stepId}.md`));
53
+ if (!notes && !verify && !step) return null;
54
+ const parts = [
55
+ "# 本项目的补充说明",
56
+ "",
57
+ "以下内容由项目维护者编写,说明这个项目的约定和要求。与前文的通用要求冲突时,以这里为准;但\"如实报告\"和安全方面的要求始终有效。",
58
+ ];
59
+ if (notes) parts.push("", notes);
60
+ if (verify) {
61
+ parts.push(
62
+ "",
63
+ "## 本项目的验证方法",
64
+ "",
65
+ "来自 `.requirement-pipeline/verify.md`,其中的命令已由项目维护者实际运行过。安装依赖、启动服务、运行测试时优先照此执行;发现它过时或不准确时,照常完成本步骤,并在你的产物里写明哪里不对、实际可行的做法是什么。",
66
+ "",
67
+ verify,
68
+ );
69
+ }
70
+ if (step) parts.push("", "## 针对本步骤", "", step);
71
+ return parts.join("\n");
72
+ }
@@ -0,0 +1,61 @@
1
+ // Machine-wide limit on pipelines running at once (maxConcurrentPipelines).
2
+ // A pipeline takes one of N slot files in ~/.req-kit/slots; creating a file with "wx"
3
+ // is atomic, so two pipelines starting together cannot both take the last slot.
4
+ // A slot whose process is gone is free again.
5
+
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { kitHome } from "./locations.js";
9
+ import { isAlive } from "./state.js";
10
+
11
+ export interface SlotHolder {
12
+ pid: number;
13
+ repo: string;
14
+ featureDir: string;
15
+ since: string;
16
+ }
17
+
18
+ export function slotsDir(env: NodeJS.ProcessEnv = process.env): string {
19
+ return path.join(kitHome(env), "slots");
20
+ }
21
+
22
+ function readHolder(file: string): SlotHolder | null {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(file, "utf8")) as SlotHolder;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /** Pipelines holding a slot now (dead holders are cleared on the way). */
31
+ export function slotHolders(dir: string): SlotHolder[] {
32
+ if (!fs.existsSync(dir)) return [];
33
+ const holders: SlotHolder[] = [];
34
+ for (const name of fs.readdirSync(dir)) {
35
+ const file = path.join(dir, name);
36
+ const holder = readHolder(file);
37
+ if (holder && isAlive(holder.pid)) holders.push(holder);
38
+ else fs.rmSync(file, { force: true });
39
+ }
40
+ return holders;
41
+ }
42
+
43
+ /** Takes a free slot; returns a release function, or null when all `max` slots are taken. */
44
+ export function tryAcquireSlot(dir: string, max: number, holder: SlotHolder): (() => void) | null {
45
+ fs.mkdirSync(dir, { recursive: true });
46
+ for (let i = 0; i < max; i++) {
47
+ const file = path.join(dir, `slot-${i}.json`);
48
+ const current = readHolder(file);
49
+ if (current && current.pid !== holder.pid && isAlive(current.pid)) continue;
50
+ if (current) fs.rmSync(file, { force: true });
51
+ try {
52
+ fs.writeFileSync(file, JSON.stringify(holder), { flag: "wx" });
53
+ } catch {
54
+ continue; // taken a moment ago by another pipeline
55
+ }
56
+ return () => {
57
+ if (readHolder(file)?.pid === holder.pid) fs.rmSync(file, { force: true });
58
+ };
59
+ }
60
+ return null;
61
+ }
@@ -0,0 +1,14 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import type { RuntimeFactory } from "./runtime.js";
4
+
5
+ export async function loadRuntimeFactory(): Promise<RuntimeFactory> {
6
+ // Test hook: run the real orchestration against a scripted fake daemon.
7
+ const fake = process.env.REQ_PIPELINE_FAKE_RUNTIME;
8
+ if (fake) {
9
+ const module = (await import(pathToFileURL(path.resolve(fake)).href)) as { createRuntime: RuntimeFactory };
10
+ return module.createRuntime;
11
+ }
12
+ const module = await import("./paseo-runtime.js");
13
+ return module.createRuntime;
14
+ }