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,346 @@
1
+ // The long-running pipeline process. Normally started by launch.ts, detached
2
+ // from the chat agent's shell; can also be run in the foreground for debugging.
3
+
4
+ import { execFileSync } from "node:child_process";
5
+ import path from "node:path";
6
+ import { approveCommand, LAUNCHER, parseLaunchArgs, resumeCommand, statusCommand, verifyCommand } from "./cli-args.js";
7
+ import { dequeuedMessage, queuedMessage, watchdogPrompt } from "./messages.js";
8
+ import { slotHolders, slotsDir, tryAcquireSlot } from "./queue.js";
9
+ import { cronEvery, parseStartTime } from "./schedule-time.js";
10
+ import { loadConfig, PHASE_MODE_LABELS, PROFILE_LABELS, REQUIREMENT_TYPE_LABELS } from "./config.js";
11
+ import { defaultConfigPath, kitVersion, localPromptsDir, PRINCIPLES_INDEX, PROMPTS_DIR } from "./locations.js";
12
+ import { satisfies } from "./daemon-check.js";
13
+ import { describeAgentProfile, resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
14
+ import { jointProblem } from "./joint.js";
15
+ import { runPipeline } from "./pipeline.js";
16
+ import { preflight, verifyPhaseProblem } from "./preflight.js";
17
+ import { loadRuntimeFactory } from "./runtime-loader.js";
18
+ import {
19
+ acquireLock,
20
+ ensurePipelineDir,
21
+ hashFile,
22
+ type PipelineState,
23
+ pipelineDir,
24
+ type RunPhases,
25
+ STATE_VERSION,
26
+ statePath,
27
+ writeState,
28
+ } from "./state.js";
29
+
30
+ function headCommit(repo: string): string | null {
31
+ try {
32
+ return execFileSync("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function log(message: string): void {
39
+ console.log(`[${new Date().toISOString()}] ${message}`);
40
+ }
41
+
42
+ async function main(): Promise<number> {
43
+ const args = parseLaunchArgs(process.argv.slice(2));
44
+ const configPath = args.configPath ?? defaultConfigPath();
45
+ const checked = preflight(args, hashFile);
46
+ const { repo, featureDir, mode } = checked;
47
+ const loaded = loadConfig(configPath, repo);
48
+ const { sources, ignored } = loaded;
49
+ let config = loaded.config;
50
+ log(`req-kit ${kitVersion()};配置:${sources.join(" → ")}`);
51
+ if (ignored.length > 0) log(`${ignored.join("、")} 只能在本机配置中设置,已忽略`);
52
+ if (config.requiredVersion && !satisfies(kitVersion(), config.requiredVersion)) {
53
+ log(`提醒:团队或项目要求 req-kit ${config.requiredVersion},当前为 ${kitVersion()}。流水线照常运行,请找时间升级(见 /req-doctor)。`);
54
+ }
55
+
56
+ ensurePipelineDir(repo, featureDir);
57
+ const release = acquireLock(repo, featureDir);
58
+ const stateFile = statePath(repo, featureDir);
59
+ const now = new Date().toISOString();
60
+ const specHash = hashFile(checked.specPath);
61
+
62
+ const createRuntime = await loadRuntimeFactory();
63
+ const runtime = await createRuntime({ config, log });
64
+
65
+ // A resume without --origin-agent keeps reporting to the chat that started the run.
66
+ const originAgentId = args.originAgentId ?? (mode.kind === "fresh" ? null : mode.previous.originAgentId);
67
+
68
+ const previousState = mode.kind === "fresh" ? null : mode.previous;
69
+ const reuse = mode.kind === "resume-reuse";
70
+ // Execution mode: --phases, else what this run already uses (continuing), else the spec, else the config.
71
+ let phaseMode =
72
+ args.phases ?? (reuse ? previousState?.phases?.mode : null) ?? checked.specPhases ?? previousState?.phases?.mode ?? config.phases.mode;
73
+ // --verify-at only makes sense with a gate; asking for it implies one.
74
+ if (args.verifyAt && phaseMode === "continuous" && !args.phases) phaseMode = "gated";
75
+ // A playbook type the configuration turned off runs the normal flow.
76
+ const type = checked.specType && config.requirementTypes.includes(checked.specType) ? checked.specType : null;
77
+ // An assessment writes no code: there is nothing to hand over for testing.
78
+ if (type === "assessment") phaseMode = "continuous";
79
+ // Joint requirement: other repositories this one also changes (an assessment only reads them).
80
+ const jointNames = type === "assessment" ? [] : checked.specJoint;
81
+ const phases: RunPhases = {
82
+ mode: phaseMode,
83
+ environment: args.verifyEnv ?? previousState?.phases?.environment ?? config.phases.verify.environment,
84
+ defects: args.defects ?? previousState?.phases?.defects ?? config.phases.verify.defects,
85
+ };
86
+ const verifyAtText = args.verifyAt ?? null;
87
+ const verifyAt = verifyAtText ? (parseStartTime(verifyAtText) ?? (Number.isNaN(Date.parse(verifyAtText)) ? null : new Date(verifyAtText))) : null;
88
+
89
+ // Checked before the state file is touched, so a refused launch leaves no record behind.
90
+ {
91
+ try {
92
+ // In a worktree created for the requirement, the chat stays in the checkout it was cut from.
93
+ const chatDir = args.sourceRepo ?? (mode.kind === "fresh" ? null : mode.previous.worktree?.sourceRepo) ?? repo;
94
+ if (verifyAtText && !verifyAt) throw new Error(`看不懂测试验证的开始时间"${verifyAtText}"`);
95
+ if (verifyAtText && phases.mode === "develop-only") throw new Error("只做研发时没有测试验证阶段,--verify-at 不适用");
96
+ if (type === "assessment" && (verifyAtText || args.phase === "verify")) throw new Error("评估类需求只做调研、不写代码,没有测试验证阶段");
97
+ const jointIssue = jointProblem(jointNames, config.relatedRepos, chatDir);
98
+ if (jointIssue) throw new Error(jointIssue);
99
+ if (checked.entersVerify) {
100
+ const problem = verifyPhaseProblem(repo, featureDir, phases.environment);
101
+ if (problem) throw new Error(problem);
102
+ }
103
+ if (originAgentId) await runtime.verifyOrigin(originAgentId, chatDir);
104
+ if (usesPaseoProfiles(config)) config = resolveConfigProfiles(config, await runtime.listPaseoProfiles());
105
+ } catch (error) {
106
+ const reason = (error as Error).message;
107
+ log(`未启动:${reason}`);
108
+ if (originAgentId) {
109
+ await runtime
110
+ .notifyAgent(originAgentId, `【需求开发流水线通知】这条消息来自后台流水线,不是用户发的。流水线没有启动:${reason}`)
111
+ .catch(() => undefined);
112
+ }
113
+ await runtime.close().catch(() => undefined);
114
+ release();
115
+ return 2;
116
+ }
117
+ }
118
+
119
+ log(`builder:${describeAgentProfile(config.agents.builder)};reviewer:${describeAgentProfile(config.agents.reviewer)}`);
120
+
121
+ const previous = mode.kind === "fresh" ? null : mode.previous;
122
+ const profile =
123
+ args.profile ?? (mode.kind === "resume-reuse" ? previous?.profile : null) ?? checked.specProfile ?? config.defaultProfile;
124
+
125
+ let state: PipelineState;
126
+ if (mode.kind === "fresh") {
127
+ state = {
128
+ version: STATE_VERSION,
129
+ repo,
130
+ featureDir,
131
+ status: "running",
132
+ attempt: 1,
133
+ specHash,
134
+ originAgentId,
135
+ pid: process.pid,
136
+ startedAt: now,
137
+ updatedAt: now,
138
+ results: {},
139
+ history: [],
140
+ stopReason: null,
141
+ outcome: null,
142
+ profile,
143
+ branch: checked.branch,
144
+ milestones: [],
145
+ incremental: null,
146
+ worktree: args.sourceRepo && args.workspaceId ? { workspaceId: args.workspaceId, sourceRepo: args.sourceRepo } : null,
147
+ phases,
148
+ phase: "develop",
149
+ verifyAt: verifyAt?.toISOString() ?? null,
150
+ handbacks: 0,
151
+ type,
152
+ };
153
+ log(
154
+ `开始新的流水线:${featureDir}(${PROFILE_LABELS[profile]}档,${type ? `类型:${REQUIREMENT_TYPE_LABELS[type]},` : ""}执行方式:${PHASE_MODE_LABELS[phases.mode]},分支 ${checked.branch ?? "无"})`,
155
+ );
156
+ } else {
157
+ const startOver = mode.kind !== "resume-reuse";
158
+ const previousImplemented = Object.keys(mode.previous.results).some((k) => k === "implement") ||
159
+ mode.previous.history.some((h) => h.stepId === "implement" && h.outcome === "completed");
160
+ state = {
161
+ ...mode.previous,
162
+ status: "running",
163
+ attempt: startOver ? mode.previous.attempt + 1 : mode.previous.attempt,
164
+ specHash,
165
+ // A later launch from a different chat moves notifications to that chat.
166
+ originAgentId,
167
+ pid: process.pid,
168
+ results: startOver ? {} : mode.previous.results,
169
+ stopReason: null,
170
+ outcome: null,
171
+ profile,
172
+ branch: checked.branch,
173
+ // Older state files have no milestones; a fresh attempt sends them again.
174
+ milestones: startOver ? [] : (mode.previous.milestones ?? []),
175
+ incremental:
176
+ mode.kind === "resume-from-design"
177
+ ? { fromAttempt: mode.previous.attempt, reason: mode.reason, baseCommit: headCommit(repo), hadImplementation: previousImplemented }
178
+ : mode.kind === "restart"
179
+ ? null
180
+ : (mode.previous.incremental ?? null),
181
+ phases,
182
+ // The verify phase starts when development is done; any other start over goes back to development.
183
+ phase: checked.entersVerify ? "verify" : startOver ? "develop" : (mode.previous.phase ?? "develop"),
184
+ verifyAt: checked.entersVerify ? null : (verifyAt?.toISOString() ?? (startOver ? null : (mode.previous.verifyAt ?? null))),
185
+ handbacks: mode.kind === "restart" ? 0 : (mode.previous.handbacks ?? 0),
186
+ type,
187
+ codeBase: startOver ? null : (mode.previous.codeBase ?? null),
188
+ // --approve: the requester confirmed the design that was waiting at the confirmation point.
189
+ approval:
190
+ args.approve && mode.previous.status === "waiting_for_approval"
191
+ ? { attempt: mode.previous.attempt, status: "approved", at: now }
192
+ : startOver
193
+ ? null
194
+ : (mode.previous.approval ?? null),
195
+ gate: mode.kind === "restart" ? null : (mode.previous.gate ?? null),
196
+ };
197
+ if (checked.entersVerify) {
198
+ // A develop-only run that ends with its development report gets a full report after verification.
199
+ delete state.results.report;
200
+ log(`开始测试验证阶段(${phases.environment === "deployed" ? "已部署的测试环境" : "本地"},缺陷处理:${phases.defects})`);
201
+ }
202
+ const why =
203
+ mode.kind === "resume-from-design"
204
+ ? mode.reason
205
+ : mode.kind === "restart"
206
+ ? "按 --restart 从头开始"
207
+ : args.approve
208
+ ? "需求方已确认方案,开始写代码"
209
+ : "从中断处继续,已完成的步骤沿用结果";
210
+ log(`继续流水线:${featureDir}(第 ${state.attempt} 次尝试,${PROFILE_LABELS[profile]}档)—— ${why}`);
211
+ }
212
+ writeState(stateFile, state);
213
+ if (state.worktree) log(`在为这个需求新建的工作区中开发:${repo}(Paseo 工作区 ${state.worktree.workspaceId},对话所在目录 ${state.worktree.sourceRepo})`);
214
+
215
+ if (state.worktree && mode.kind === "fresh") {
216
+ // paseo.json's worktree.setup (installing dependencies, copying .env) runs in the background.
217
+ const setup = await runtime.waitForWorkspaceSetup(state.worktree.workspaceId, 30 * 60_000).catch(() => ({ status: "none" as const, error: null }));
218
+ if (setup.status === "completed") log("新工作区的环境准备(paseo.json)已完成");
219
+ else if (setup.status === "failed" || setup.status === "timeout") {
220
+ const why = setup.status === "timeout" ? "30 分钟内没有完成" : `失败:${setup.error ?? "原因未知"}`;
221
+ log(`新工作区的环境准备(paseo.json)${why}。流水线继续,步骤会按验证方法自行准备环境。`);
222
+ }
223
+ }
224
+
225
+ // The chat's own directory: a tester's chat may sit in another checkout of the same repository.
226
+ const chatRepo = args.sourceRepo ?? state.worktree?.sourceRepo ?? repo;
227
+ const commands = {
228
+ resume: resumeCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
229
+ status: statusCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
230
+ verify: verifyCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
231
+ approve: approveCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
232
+ };
233
+ const notify = async (text: string) => {
234
+ log(`通知:\n${text}`);
235
+ if (originAgentId) await runtime.notifyAgent(originAgentId, text).catch((error: Error) => log(`通知发送失败:${error.message}`));
236
+ };
237
+
238
+ // Reminders and automatic continuations from the previous run are no longer needed.
239
+ if (state.schedules?.length) {
240
+ for (const s of state.schedules) await runtime.cancelSchedule(s.id).catch((error: Error) => log(`取消定时任务 ${s.id} 失败:${error.message}`));
241
+ log(`已取消上次运行留下的 Paseo 定时任务:${state.schedules.map((s) => s.kind).join("、")}`);
242
+ state.schedules = [];
243
+ writeState(stateFile, state);
244
+ }
245
+
246
+ let releaseSlot = () => {};
247
+ const shutdown = (signal: string) => {
248
+ log(`收到 ${signal},流水线停止`);
249
+ state.status = "failed";
250
+ state.stopReason = `流水线进程被 ${signal} 终止`;
251
+ state.queuedSince = null;
252
+ writeState(stateFile, state);
253
+ releaseSlot();
254
+ release();
255
+ process.exit(1);
256
+ };
257
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
258
+ process.once("SIGINT", () => shutdown("SIGINT"));
259
+
260
+ // Machine-wide limit on concurrent pipelines: wait for a free slot.
261
+ if (config.maxConcurrentPipelines > 0) {
262
+ const dir = slotsDir();
263
+ const holder = { pid: process.pid, repo, featureDir, since: new Date().toISOString() };
264
+ let slot = tryAcquireSlot(dir, config.maxConcurrentPipelines, holder);
265
+ if (!slot) {
266
+ const since = Date.now();
267
+ state.queuedSince = new Date(since).toISOString();
268
+ writeState(stateFile, state);
269
+ log(`本机同时运行的流水线已达上限(${config.maxConcurrentPipelines}),排队等待`);
270
+ await notify(queuedMessage({ featureDir, max: config.maxConcurrentPipelines, holders: slotHolders(dir) }));
271
+ while (!slot) {
272
+ await new Promise((resolve) => setTimeout(resolve, config.queuePollSeconds * 1000));
273
+ slot = tryAcquireSlot(dir, config.maxConcurrentPipelines, holder);
274
+ }
275
+ state.queuedSince = null;
276
+ writeState(stateFile, state);
277
+ log("排队结束,开始运行");
278
+ await notify(dequeuedMessage({ featureDir, waitedMinutes: (Date.now() - since) / 60_000 }));
279
+ }
280
+ releaseSlot = slot;
281
+ }
282
+
283
+ // Optional watchdog: the chat checks on the run now and then and continues it if the process died.
284
+ let watchdog: string | null = null;
285
+ if (config.watchdogMinutes > 0 && originAgentId) {
286
+ watchdog = await runtime
287
+ .scheduleMessage({
288
+ agentId: originAgentId,
289
+ cron: cronEvery(config.watchdogMinutes),
290
+ expiresAt: new Date(Date.now() + 3 * 24 * 3600_000),
291
+ name: `req-kit 定时检查:${path.basename(featureDir)}`,
292
+ prompt: watchdogPrompt({ featureDir, statusCommand: commands.status, resumeCommand: commands.resume }),
293
+ })
294
+ .catch((error: Error) => {
295
+ log(`预约定时检查失败:${error.message}`);
296
+ return null;
297
+ });
298
+ if (watchdog) {
299
+ state.schedules = [...(state.schedules ?? []), { id: watchdog, kind: "watchdog", at: null }];
300
+ writeState(stateFile, state);
301
+ log(`已预约定时检查(每 ${config.watchdogMinutes} 分钟,Paseo 定时任务 ${watchdog})`);
302
+ }
303
+ }
304
+
305
+ try {
306
+ const outcome = await runPipeline({
307
+ repo,
308
+ featureDir,
309
+ config,
310
+ runtime,
311
+ state,
312
+ statePath: stateFile,
313
+ prompts: { promptsDir: PROMPTS_DIR, localPromptsDir: localPromptsDir(), principlesFile: PRINCIPLES_INDEX },
314
+ logPath: path.join(pipelineDir(repo, featureDir), "pipeline.log"),
315
+ // Run from the chat, whose directory is the source checkout when the requirement has its own worktree.
316
+ resumeCommand: commands.resume,
317
+ statusCommand: commands.status,
318
+ verifyCommand: commands.verify,
319
+ approveCommand: commands.approve,
320
+ jointNames,
321
+ log,
322
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
323
+ });
324
+ log(`流水线结束:${outcome}`);
325
+ return outcome === "completed" || outcome === "phase_done" ? 0 : outcome === "waiting_for_business" || outcome === "waiting_for_approval" ? 3 : 1;
326
+ } finally {
327
+ // The run ended by itself: the watchdog has nothing left to watch.
328
+ if (watchdog) {
329
+ await runtime.cancelSchedule(watchdog).catch(() => undefined);
330
+ state.schedules = (state.schedules ?? []).filter((s) => s.id !== watchdog);
331
+ writeState(stateFile, state);
332
+ }
333
+ await runtime.close().catch(() => undefined);
334
+ releaseSlot();
335
+ release();
336
+ }
337
+ }
338
+
339
+ main().then(
340
+ (code) => process.exit(code),
341
+ (error: Error) => {
342
+ console.error(`[${new Date().toISOString()}] 流水线无法启动:${error.message}`);
343
+ console.error(`启动命令:${LAUNCHER}`);
344
+ process.exit(2);
345
+ },
346
+ );