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,405 @@
1
+ // Entry point for the chat agent.
2
+ // start: validates the request, then starts main.ts in its own session (detached)
3
+ // so the pipeline outlives the agent's shell and turn; returns in a second or two.
4
+ // --status: prints progress in plain language.
5
+ // --stop: stops the pipeline and its current step agent; --resume continues later.
6
+ // --list: lists every requirement in this repository's worktrees.
7
+
8
+ import { spawn } from "node:child_process";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { launchArgv, parseLaunchArgs, startCommand, verifyCommand } from "./cli-args.js";
12
+ import { scheduledStartPrompt } from "./messages.js";
13
+ import { describeTime, parseStartTime } from "./schedule-time.js";
14
+ import { moveToOwnWorktree, type OwnWorktree } from "./worktree.js";
15
+ import { loadConfig, PHASE_MODE_LABELS, type PipelineConfig, PROFILE_LABELS } from "./config.js";
16
+ import { defaultConfigPath, ENGINE_DIR } from "./locations.js";
17
+ import { describeLiveActivity, describeStatus, stopPipeline } from "./control.js";
18
+ import { describeFeatures, featuresWithRuns, findWorktreeRun, listFeatures, resolveFeatureArg } from "./features.js";
19
+ import { checkChatCwd, normalizeFeatureDir, preflight, type PreflightResult, specProfile, verifyPhaseProblem } from "./preflight.js";
20
+ import { loadRuntimeFactory } from "./runtime-loader.js";
21
+ import type { AgentRuntime } from "./runtime.js";
22
+ import { ensurePipelineDir, hashFile, isAlive, pipelineDir, readState, statePath } from "./state.js";
23
+
24
+ function fail(message: string): never {
25
+ console.error(`未启动:${message}`);
26
+ process.exit(2);
27
+ }
28
+
29
+ interface Reservation {
30
+ scheduleId: string;
31
+ at: string;
32
+ command: string;
33
+ createdAt: string;
34
+ }
35
+
36
+ function reservationFile(repo: string, featureDir: string): string {
37
+ return path.join(pipelineDir(repo, featureDir), "scheduled-start.json");
38
+ }
39
+
40
+ function readReservation(repo: string, featureDir: string): Reservation | null {
41
+ try {
42
+ return JSON.parse(fs.readFileSync(reservationFile(repo, featureDir), "utf8")) as Reservation;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /** Runs `use` with a runtime, giving up after `timeoutMs` (the daemon may be unreachable). */
49
+ async function withRuntime<T>(config: PipelineConfig, use: (runtime: AgentRuntime) => Promise<T>, timeoutMs = 60_000): Promise<T> {
50
+ const work = (async () => {
51
+ const runtime = await (await loadRuntimeFactory())({ config, log: () => undefined });
52
+ try {
53
+ return await use(runtime);
54
+ } finally {
55
+ await runtime.close().catch(() => undefined);
56
+ }
57
+ })();
58
+ return Promise.race([
59
+ work,
60
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error("连接 Paseo 超时")), timeoutMs).unref()),
61
+ ]);
62
+ }
63
+
64
+ /** Cancels a scheduled start, if there is one. Returns it. */
65
+ async function dropReservation(repo: string, featureDir: string, config: PipelineConfig): Promise<Reservation | null> {
66
+ const reservation = readReservation(repo, featureDir);
67
+ if (!reservation) return null;
68
+ await withRuntime(config, (runtime) => runtime.cancelSchedule(reservation.scheduleId)).catch(() => undefined);
69
+ fs.rmSync(reservationFile(repo, featureDir), { force: true });
70
+ return reservation;
71
+ }
72
+
73
+ const argv = process.argv.slice(2);
74
+ let args;
75
+ try {
76
+ args = parseLaunchArgs(argv);
77
+ } catch (error) {
78
+ fail((error as Error).message);
79
+ }
80
+ const configPath = args.configPath ?? defaultConfigPath();
81
+
82
+ if (args.list) {
83
+ console.log(describeFeatures(listFeatures(args.repo), args.repo));
84
+ process.exit(0);
85
+ }
86
+
87
+ // A requirement started here with --worktree lives in the worktree created for it; the
88
+ // chat stays here, so status, stop and resume from this chat are redirected there.
89
+ if (args.featureDir && fs.existsSync(args.repo) && !args.sourceRepo && !args.worktree && !args.specReview) {
90
+ const here = (() => {
91
+ try {
92
+ return resolveFeatureArg(args.repo, args.featureDir);
93
+ } catch {
94
+ return null;
95
+ }
96
+ })();
97
+ const hasRunHere = here !== null && fs.existsSync(statePath(args.repo, here));
98
+ const away = hasRunHere ? null : findWorktreeRun(args.repo, here ?? args.featureDir);
99
+ if (away) {
100
+ args.sourceRepo = fs.realpathSync(args.repo);
101
+ args.repo = away.repo;
102
+ args.featureDir = away.featureDir;
103
+ args.workspaceId = away.workspaceId;
104
+ }
105
+ }
106
+
107
+ // Short names: "sms-login" for docs/features/20261012-sms-login.
108
+ if (args.featureDir && fs.existsSync(args.repo)) {
109
+ try {
110
+ args.featureDir = resolveFeatureArg(args.repo, args.featureDir);
111
+ } catch (error) {
112
+ if (args.status || args.stop) {
113
+ console.error((error as Error).message);
114
+ process.exit(2);
115
+ }
116
+ fail((error as Error).message);
117
+ }
118
+ }
119
+
120
+ // --status without --feature: the only requirement here that has run, else the list.
121
+ if (args.status && !args.featureDir) {
122
+ const ran = fs.existsSync(args.repo) ? featuresWithRuns(args.repo) : [];
123
+ if (ran.length !== 1) {
124
+ if (ran.length > 1) console.log("这个工作区里有多个需求运行过,请指定要看哪一个(--feature 后写名称即可)。\n");
125
+ console.log(describeFeatures(listFeatures(args.repo), args.repo));
126
+ process.exit(0);
127
+ }
128
+ args.featureDir = ran[0];
129
+ }
130
+
131
+ if (args.status || args.stop) {
132
+ let repo: string;
133
+ let featureDir: string;
134
+ try {
135
+ repo = fs.realpathSync(args.repo);
136
+ featureDir = normalizeFeatureDir(repo, args.featureDir);
137
+ } catch (error) {
138
+ console.error((error as Error).message);
139
+ process.exit(2);
140
+ }
141
+ const file = statePath(repo, featureDir);
142
+ if (args.status) {
143
+ const state = readState(file);
144
+ const reservation = readReservation(repo, featureDir);
145
+ if (!state && reservation) {
146
+ console.log(`需求:${featureDir}\n状态:已预约在 ${describeTime(new Date(reservation.at))} 开始开发(Paseo 定时任务,到时由这个对话执行启动命令)。取消预约:执行暂停命令。`);
147
+ process.exit(0);
148
+ }
149
+ console.log(describeStatus(state, featureDir));
150
+ if (state && reservation) console.log(`已预约:${describeTime(new Date(reservation.at))} 开始(Paseo 定时任务,到时由对话执行;执行暂停命令可以取消)`);
151
+ if (args.sourceRepo) console.log(`位置:为这个需求新建的工作区 ${repo}(需求文件在 ${path.join(repo, featureDir)})`);
152
+ // Live detail from Paseo, best effort: the plain status above never depends on the daemon.
153
+ if (state?.status === "running") {
154
+ const fetchLive = async (): Promise<string[]> => {
155
+ const runtime = await (await loadRuntimeFactory())({ config: loadConfig(configPath, repo).config, log: () => undefined });
156
+ try {
157
+ return await describeLiveActivity(state, runtime);
158
+ } finally {
159
+ await runtime.close().catch(() => undefined);
160
+ }
161
+ };
162
+ // Daemon unreachable or config unreadable: the status above is still correct.
163
+ const live = await Promise.race([fetchLive().catch(() => []), new Promise<string[]>((resolve) => setTimeout(() => resolve([]), 10_000).unref())]);
164
+ if (live.length > 0) console.log(`\n${live.join("\n")}`);
165
+ }
166
+ process.exit(0);
167
+ }
168
+ let config;
169
+ try {
170
+ config = loadConfig(configPath, repo).config;
171
+ } catch (error) {
172
+ console.error((error as Error).message);
173
+ process.exit(2);
174
+ }
175
+ const reservation = await dropReservation(repo, featureDir, config);
176
+ if (reservation) console.log(`已取消预约在 ${describeTime(new Date(reservation.at))} 的开始。`);
177
+ if (reservation && !readState(file)) process.exit(0);
178
+ const result = await stopPipeline({
179
+ statePath: file,
180
+ featureDir,
181
+ config,
182
+ createRuntime: await loadRuntimeFactory(),
183
+ log: (message) => console.error(message),
184
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
185
+ });
186
+ console.log(result.message);
187
+ process.exit(result.stopped ? 0 : 1);
188
+ }
189
+
190
+ if (args.specReview) {
191
+ let repo: string;
192
+ let featureDir: string;
193
+ let reviewConfig: PipelineConfig;
194
+ try {
195
+ repo = fs.realpathSync(args.repo);
196
+ checkChatCwd(args, repo);
197
+ featureDir = normalizeFeatureDir(repo, args.featureDir);
198
+ reviewConfig = loadConfig(configPath, repo).config;
199
+ } catch (error) {
200
+ fail((error as Error).message);
201
+ }
202
+ const specFile = path.join(repo, featureDir, "spec.md");
203
+ if (!fs.existsSync(specFile)) fail(`功能目录中没有 spec.md:${featureDir}`);
204
+ const reviewProfile = specProfile(fs.readFileSync(specFile, "utf8")) ?? reviewConfig.defaultProfile;
205
+ if (reviewConfig.specReview === "off" || (reviewConfig.specReview === "standard" && reviewProfile === "quick")) {
206
+ console.log(
207
+ reviewConfig.specReview === "off"
208
+ ? "跳过:配置中关闭了需求说明审阅(specReview)。直接请用户确认需求说明即可。"
209
+ : "跳过:快速档的需求不做独立审阅。直接请用户确认需求说明即可。",
210
+ );
211
+ process.exit(0);
212
+ }
213
+ const current = readState(statePath(repo, featureDir));
214
+ if (current?.status === "running" && isAlive(current.pid)) fail("这个需求的开发流水线正在运行,不需要再审阅需求说明");
215
+ ensurePipelineDir(repo, featureDir);
216
+ const reviewLog = path.join(pipelineDir(repo, featureDir), "spec-review.log");
217
+ const reviewOut = fs.openSync(reviewLog, "a");
218
+ const reviewer = spawn(process.execPath, [...process.execArgv, path.join(ENGINE_DIR, "src", "spec-review.ts"), ...launchArgv({ ...args, featureDir })], {
219
+ cwd: repo,
220
+ detached: true,
221
+ windowsHide: true,
222
+ stdio: ["ignore", reviewOut, reviewOut],
223
+ env: process.env,
224
+ });
225
+ reviewer.unref();
226
+ console.log(
227
+ [
228
+ "已开始需求说明的独立审阅(后台进行,通常需要几分钟)。",
229
+ `功能目录:${featureDir}`,
230
+ `审阅结果会写到 ${featureDir}/spec-review.md,完成后会作为一条消息发回这个对话。`,
231
+ `日志:${reviewLog}`,
232
+ "等待期间可以继续和用户确认其他细节,但先不要把需求说明标为已确认。",
233
+ ].join("\n"),
234
+ );
235
+ process.exit(0);
236
+ }
237
+
238
+ let checked: PreflightResult;
239
+ try {
240
+ checked = preflight(args, hashFile);
241
+ } catch (error) {
242
+ fail((error as Error).message);
243
+ }
244
+
245
+ let loaded;
246
+ try {
247
+ loaded = loadConfig(configPath, checked.repo);
248
+ } catch (error) {
249
+ fail((error as Error).message);
250
+ }
251
+
252
+ if (args.verifyAt) {
253
+ const at = parseStartTime(args.verifyAt);
254
+ if (!at) fail(`看不懂测试验证的开始时间"${args.verifyAt}"。可以写 22:00、明天 8:30、2026-10-14 08:30 或 +2h`);
255
+ if (!args.originAgentId) fail("预约测试验证需要在 Paseo 的对话中执行:到时由这个对话执行开始测试验证的命令。");
256
+ if (args.phases === "develop-only") fail("只做研发时没有测试验证阶段,--verify-at 不适用");
257
+ args.verifyAt = at.toISOString();
258
+ }
259
+
260
+ if (checked.entersVerify) {
261
+ const previous = checked.mode.kind === "fresh" ? null : checked.mode.previous;
262
+ const environment = args.verifyEnv ?? previous?.phases?.environment ?? loaded.config.phases.verify.environment;
263
+ const problem = verifyPhaseProblem(checked.repo, checked.featureDir, environment);
264
+ if (problem) fail(problem);
265
+ }
266
+
267
+ if (args.at) {
268
+ const at = parseStartTime(args.at);
269
+ if (!at) fail(`看不懂开始时间"${args.at}"。可以写 22:00、明天 8:30、2026-10-14 08:30 或 +2h`);
270
+ if (at.getTime() < Date.now() + 60_000) fail("开始时间要在至少 1 分钟之后");
271
+ const origin = args.originAgentId;
272
+ if (!origin) fail("预约开始需要在 Paseo 的对话中执行:到时由这个对话执行启动命令。");
273
+ const command = args.phase
274
+ ? verifyCommand({ repo: checked.repo, featureDir: checked.featureDir, configPath: args.configPath, verifyEnv: args.verifyEnv, defects: args.defects })
275
+ : startCommand({
276
+ repo: checked.repo,
277
+ featureDir: checked.featureDir,
278
+ configPath: args.configPath,
279
+ profile: args.profile,
280
+ worktree: args.worktree,
281
+ phases: args.phases,
282
+ verifyAt: args.verifyAt,
283
+ });
284
+ let scheduleId: string | null;
285
+ try {
286
+ await dropReservation(checked.repo, checked.featureDir, loaded.config);
287
+ scheduleId = await withRuntime(loaded.config, (runtime) =>
288
+ runtime.scheduleMessage({
289
+ agentId: origin,
290
+ at,
291
+ name: `req-kit 预约${args.phase ? "测试验证" : "开发"}:${path.basename(checked.featureDir)}`,
292
+ prompt: scheduledStartPrompt({ featureDir: checked.featureDir, command }),
293
+ }),
294
+ );
295
+ } catch (error) {
296
+ fail(`没能预约:${(error as Error).message}`);
297
+ }
298
+ if (!scheduleId) fail("这个 Paseo 版本不支持定时任务,无法预约。可以到时再启动。");
299
+ ensurePipelineDir(checked.repo, checked.featureDir);
300
+ const reservation: Reservation = { scheduleId, at: at.toISOString(), command, createdAt: new Date().toISOString() };
301
+ fs.writeFileSync(reservationFile(checked.repo, checked.featureDir), `${JSON.stringify(reservation, null, 2)}\n`);
302
+ console.log(
303
+ [
304
+ `已预约在 ${describeTime(at)} 开始${args.phase ? "测试验证" : "开发"}(Paseo 定时任务,在 Paseo 的 Schedules 中可以看到)。到时会在这个对话里自动执行启动命令。`,
305
+ `功能目录:${checked.featureDir}`,
306
+ "查看:查看进度命令;取消:暂停命令。在那之前修改需求说明也没关系,开始时以最新的为准。",
307
+ ].join("\n"),
308
+ );
309
+ process.exit(0);
310
+ }
311
+
312
+ // Starting now: a start scheduled earlier (or this start, fired by it) is no longer needed.
313
+ if (readReservation(checked.repo, checked.featureDir)) await dropReservation(checked.repo, checked.featureDir, loaded.config);
314
+
315
+ let ownWorktree: OwnWorktree | null = null;
316
+ if (args.worktree) {
317
+ // Create the requirement's own worktree now, so the pipeline starts there.
318
+ const spec = fs.readFileSync(checked.specPath, "utf8");
319
+ const title = (/^#\s+(.+)$/m.exec(spec)?.[1] ?? path.basename(checked.featureDir)).trim();
320
+ const create = async () => {
321
+ const runtime = await (await loadRuntimeFactory())({ config: loaded.config, log: () => undefined });
322
+ try {
323
+ return await moveToOwnWorktree({ runtime, sourceRepo: checked.repo, featureDir: checked.featureDir, originAgentId: args.originAgentId, title });
324
+ } finally {
325
+ await runtime.close().catch(() => undefined);
326
+ }
327
+ };
328
+ try {
329
+ ownWorktree = await Promise.race([
330
+ create(),
331
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error("连接 Paseo 新建工作区超过 2 分钟没有完成")), 120_000).unref()),
332
+ ]);
333
+ } catch (error) {
334
+ fail(`没能为这个需求新建工作区:${(error as Error).message}`);
335
+ }
336
+ args.sourceRepo = checked.repo;
337
+ args.repo = ownWorktree.repo;
338
+ args.workspaceId = ownWorktree.workspaceId;
339
+ args.worktree = false;
340
+ try {
341
+ checked = preflight(args, hashFile);
342
+ } catch (error) {
343
+ fail(`新工作区 ${ownWorktree.repo} 已建好,但无法在那里启动:${(error as Error).message}`);
344
+ }
345
+ }
346
+
347
+ ensurePipelineDir(checked.repo, checked.featureDir);
348
+ const logPath = path.join(pipelineDir(checked.repo, checked.featureDir), "pipeline.log");
349
+ const out = fs.openSync(logPath, "a");
350
+
351
+ // Re-use this process's loader flags (tsx registers itself through execArgv).
352
+ const child = spawn(process.execPath, [...process.execArgv, path.join(ENGINE_DIR, "src", "main.ts"), ...launchArgv(args)], {
353
+ cwd: checked.repo,
354
+ detached: true,
355
+ windowsHide: true,
356
+ stdio: ["ignore", out, out],
357
+ env: process.env,
358
+ });
359
+ child.unref();
360
+
361
+ const modeText: Record<string, string> = {
362
+ fresh: "新启动",
363
+ restart: "从头重新开始",
364
+ "resume-reuse": "从中断处继续",
365
+ "resume-from-design": "需求有变化,从方案设计开始增量处理",
366
+ };
367
+ const phaseMode =
368
+ args.phases ??
369
+ (checked.mode.kind === "resume-reuse" ? checked.mode.previous.phases?.mode : null) ??
370
+ checked.specPhases ??
371
+ (checked.mode.kind === "fresh" ? null : checked.mode.previous.phases?.mode) ??
372
+ (args.verifyAt ? "gated" : loaded.config.phases.mode);
373
+ const profile =
374
+ args.profile ??
375
+ (checked.mode.kind === "resume-reuse" ? checked.mode.previous.profile : null) ??
376
+ checked.specProfile ??
377
+ loaded.config.defaultProfile;
378
+
379
+ console.log(
380
+ [
381
+ checked.entersVerify
382
+ ? `已在后台开始测试验证阶段(${PROFILE_LABELS[profile]}档),进程 ${child.pid}。提测说明:${checked.featureDir}/handoff.md`
383
+ : `已在后台启动(${modeText[checked.mode.kind]},${PROFILE_LABELS[profile]}档${phaseMode === "continuous" ? "" : `,执行方式:${PHASE_MODE_LABELS[phaseMode]}`}),进程 ${child.pid}。`,
384
+ !checked.entersVerify && phaseMode === "gated"
385
+ ? `研发完成(写好提测说明)后会停下来通知这个对话,等待测试验证${args.verifyAt ? `;已安排在 ${describeTime(new Date(args.verifyAt))} 之后自动开始测试验证` : ""}。`
386
+ : !checked.entersVerify && phaseMode === "develop-only"
387
+ ? "只做研发:写好提测说明和研发报告后结束,测试由人完成。"
388
+ : null,
389
+ ownWorktree
390
+ ? `已为这个需求新建工作区:${ownWorktree.repo}(分支 ${ownWorktree.branch},在 Paseo 侧边栏中可以看到)。开发在那里进行,步骤仍挂在这个对话下;这个对话的工作目录不变。` +
391
+ `需求文件已移到 ${path.join(ownWorktree.repo, checked.featureDir)}${ownWorktree.removedSource ? "" : "(这里原有的已提交版本保留不动)"},之后读写需求文件请用这个路径。`
392
+ : args.sourceRepo
393
+ ? `这个需求在为它新建的工作区中开发:${checked.repo}`
394
+ : null,
395
+ `功能目录:${checked.featureDir}`,
396
+ checked.branch ? `工作分支:${checked.branch}` : "不是 git 仓库:步骤不会提交代码。",
397
+ loaded.sources.length > 1 ? `使用了项目配置:${loaded.sources[1]}` : null,
398
+ `运行日志:${logPath}`,
399
+ args.originAgentId
400
+ ? `进展和结果会发回 agent ${args.originAgentId} 的对话。`
401
+ : "没有提供 --origin-agent,进展只写入日志。",
402
+ ]
403
+ .filter(Boolean)
404
+ .join("\n"),
405
+ );
@@ -0,0 +1,55 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // Where things live.
7
+ //
8
+ // <kit root>/ one installed version of req-kit, e.g. ~/tools/req-kit/versions/1.0.0
9
+ // bin/start-pipeline.mjs launcher the /req skill runs
10
+ // engine/prompts/ step prompts shipped with this version
11
+ // principles/INDEX.md principles shared by the chat skills and the step prompts
12
+ // <kit home>/ machine settings that survive upgrades, default ~/.req-kit
13
+ // config.json machine configuration
14
+ // prompts.local/ machine-wide prompt overrides (same file names as engine/prompts)
15
+
16
+ export const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+ export const KIT_ROOT = path.resolve(ENGINE_DIR, "..");
18
+ export const LAUNCHER = path.join(KIT_ROOT, "bin", "start-pipeline.mjs");
19
+ export const PROMPTS_DIR = path.join(ENGINE_DIR, "prompts");
20
+ export const PRINCIPLES_INDEX = path.join(KIT_ROOT, "principles", "INDEX.md");
21
+
22
+ /** Machine settings directory. REQ_KIT_HOME overrides it (tests, or several setups on one machine). */
23
+ export function kitHome(env: NodeJS.ProcessEnv = process.env): string {
24
+ const override = env.REQ_KIT_HOME?.trim();
25
+ return override ? path.resolve(override) : path.join(os.homedir(), ".req-kit");
26
+ }
27
+
28
+ export function defaultConfigPath(env: NodeJS.ProcessEnv = process.env): string {
29
+ return path.join(kitHome(env), "config.json");
30
+ }
31
+
32
+ export function localPromptsDir(env: NodeJS.ProcessEnv = process.env): string {
33
+ return path.join(kitHome(env), "prompts.local");
34
+ }
35
+
36
+ export function kitVersion(): string {
37
+ try {
38
+ const pkg = JSON.parse(fs.readFileSync(path.join(KIT_ROOT, "package.json"), "utf8")) as { version?: string };
39
+ return pkg.version ?? "unknown";
40
+ } catch {
41
+ return "unknown";
42
+ }
43
+ }
44
+
45
+ /** An installed dependency, looked up like Node does (see bin/locate.mjs). */
46
+ export function findDependency(name: string, fromDir: string = KIT_ROOT): string | null {
47
+ let dir = path.resolve(fromDir);
48
+ for (;;) {
49
+ const candidate = path.join(dir, "node_modules", ...name.split("/"));
50
+ if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
51
+ const parent = path.dirname(dir);
52
+ if (parent === dir) return null;
53
+ dir = parent;
54
+ }
55
+ }